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
Proposal for API changes #803
Comments
So, firstly, for some context, here is the readme example as I'd have it. Whenever you see a Model used explicity, it could just as easily be import {Model, relation, table} from 'bookshelf';
@table('users')
User extends Model {
@relation
static messages() { return this.hasMany(Posts); }
});
@table('messages')
Posts extends Model {
@relation
static tags() { return this.belongsToMany(Tag); }
}
@table('tags')
Tag extends Model {}
User.where('id', 1).withRelated('posts.tags').fetch()
.then(user => console.log(user.related('posts').toJSON())
.catch(error => console.error(error)); The |
Now the main thing I wanted to achieve with this design is to divorce the concept of chains from the actual model state. Because I want all 'command builder' chains to come off the constructor, they absolutely must not store state. A mutable constructor function seems bad. (I'm calling them 'command builders' to distinguish them from knex's 'query builder'.) So, I came up with this concept of a I'll start from the bottom: // -- ChainFactory
// ChainFactory generates command chains that inherit from its static interface.
// Any call to `chain` will either create a new chain, or continue an existing
// one.
//
// The rule here is that every chainable method calls `chain` internally
// to get an instance of "itself". This ensure that it will make sense when
// bound to the ChainFactory constructor, and also when it's intantiated. When
// instantiated `chainInstance.chain` is overridden to return itself.
//
// This obligation to call `.chain()` is hidden from consumers of Bookshelf, as
// they can make their own chaining functions by calling `.query()` or `.where()`
// as they can now.
class ChainFactory {
static chain() {
// Create new object with this constructor as its prototype. This will give
// the `chain` the static properties Class.*
let chain = Object.create(this);
// Remove any static functions we don't want to chain. eg. `forge`.
// We just override their name on the current object, shadowing the super
// version.
//
// __notChainable is assigned by the `@doNotChain` decorator.
for (property in this) {
if (property.__notChainable) {
chain[property] = null;
}
}
// Now override the `prototype.chain` (ie. `ChainFactory.chain`), with a
// function that returns itself. This way any chained calls
// will not produce new instances.
chain.chain = function () { return this; };
// Allow state to be assigned here by inheriting classes.
if (this.initializeChain) this.initializeChain(chain);
// Return the new chain instance.
return chain;
}
}
// -- `@doNotChain` static method decoration
// Tacks on a little flag to methods that we don't want to reveal in chains.
//
// class MyThingo extends ChainFactory {
// @doNotChain
// someStaticFunction() {
// // ...
// }
// }
//
function doNotChain() {
return function decorator(staticFunction, name, descriptor) {
staticFunction.__notChainable = true;
}
} So, just say you have This doesn't make a lot of sense in isolation so I'll move on to |
So, he is the (fairly large) Model class that I've concocted. // -- Model, based on existing model, but different.
@staticAlias('q', 'query');
class Model extends ChainFactory {
// Build a new model instance. Model.forge() is preferred.
//
// Model.forge({id: 5});
// // same as
// new Model({id: 5});
//
// This is also overridden, however, to initiate a transacted save.
//
// Model.transacting(trx).where(...
//
// is the same as:
//
// Model(trx).where(...
//
constructor(attributes) {
// No need to call `super` here, we're just inheriting static properties (amazing!).
// Check if this is not being called as a constructor. Gives us the
// transaction shorthand above.
//
// Much terser than `{transacting: trx}`, but still fairly clear in the
// context of a transaction callback.
if (arguments.callee !== this.constructor) {
// If there are no arguments we return a command chain bound to a
// transaction.
let transaction = attributes;
return this.transacting(transaction);
}
// Set initial attribute state.
this.attributes = new Map();
this.set(attributes);
// Call overridden initializer.
if (this.initialize) this.initialize();
}
get id() {
if (Array.isArray(idAttribute)) {
return idAttributes.map(attr => this.attributes[attr]);
}
return this.attributes[idAttribute];
}
isNew() {
return _.isEmpty(this.id);
}
// Classic attribute mutator.
set(attributeName, value) {
_.isString(attributeName) {
this.attributes.set(attributeName, value);
} else {
let attributes = attributeName;
Object.keys(attributes).forEach((key) =>
this.attributes.set(key, attributes[key])
);
}
}
// Classic attribute accessor.
get(key) {
return this.attributes.get(key);
}
// A few passthroughs here, for convenience.
load(related, {transacting}) {
return this.constructor(transacting).withRelated(related).load(this);
}
save({withRelated, transacting}) {
if (this.isNew()) {
this.create.apply(this, arguments);
} else {
this.patch.apply(this, arguments);
}
}
patch({withRelated, transacting}) {
return this.constructor(transacting).withRelated(related).patch(this);
}
create({withRelated, transacting}) {
return this.constructor(transacting).withRelated(related).create(this);
}
// `refresh` replaces `fetch`. `fetch` only exists to terminate chains. You
// can only `refresh` models. (I kind of prefer `renew` here because it sounds
// like re-borrowing a book from a library. Thematic heh.)
refresh({transacting}) {
let attributes = this.isNew()
: this.attributes
? _.pick(this.attributes, this.idAttribute);
return this.constructor(transacting).where(attributes).fetch()
}
// -- Static non-chained members --
// Model.load(engine, 'carriages')
// // or...
// Model.load(users, 'accounts', 'messages')
//
// Doesn't support nested loading.
@doNotChain;
static load(target, ...related) {
// Accept array input.
if (_.isArray(related[0])) related = related[0];
// Bulk load each relation type.
return Promise.all(related.map(relation => {
let rel = this.relation(relation);
return rel.attach(target);
});
}
@doNotChain;
static forge(attributes) {
return new this(attributes);
}
// -- Chain members --
// Intialize the chain instance.
@doNotChain;
static initializeChain(chain) {
// `this` here is the constructor, as it's a static function. Note,
// `tableName` is attached statically to the class constructor.
chain._ModelConstructor = this;
chain._queryBuilder = knex(this.tableName);
chain._withRelated = new Set();
chain._returnSingle = true;
chain._columns = undefined;
chain._where = new Map();
}
// Set this chain to return all rows when `fetch` is called.
static all() {
let chain = this.chain();
chain._returnSingle = false;
return chain;
}
// Set this chain to return a single row when `fetch` is called.
static one() {
let chain = this.chain();
chain._returnSingle = true;
return chain;
}
// Add relations to be retrieved with the query.
// Could just be called `with`.
//
// House.where({id: 5}).with([rooms, occupants]).fetch();
//
static withRelated(relations) {
let chain = this.chain()
relations = Array.isArray(relations) ? relations : [relations];
relations.forEach(relation =>
chain._withRelated.add(relation);
);
return chain;
}
// How about using the following instead of the `object` syntax?
//
// Model.q('where', 'id', '<', 10).q('where', 'id', >, 2).all().fetch().then(//...
//
static query(method, ...args) {
let chain = this.chain();
let query = chain._queryBuilder;
// Handle 'whereIn', 'id', [0,2,3] type syntax.
if (_.isString(method) {
query[method].apply(query, args)
return chain;
// Handle callback syntax.
} else if (_.isFunction(method)) {
method.call(query, query, knex);
return chain;
// Handle terminating chain when no arguments are passed.
} else if (_.isUndefined(method)) {
return query;
// Tell user if they made a mistake.
} else {
throw new Error('unexpected argument');
}
}
static limit(limit) {
this.query('limit', limit);
}
static offset(offset) {
this.query('offset', offset);
}
// Set the current transaction.
// Actually would like to use this shortcut as well:
//
// Model(transaction).where('id', 5)...
//
// This allows `Model(null)` too, so you can write functions that are
// transaction agnostic.
//
static transacting(transaction) {
if (transaction) {
return this.query('transacting', transaction);
}
return this.chain();
}
// Just the hash argument here.
static where(attributes) {
// Do all the prefixing and formatting here so that the consumer doesn't
// have to. Also helps with DRY in Relations.
//
let formatted = this.prefixKeys(_.mapKeys(attributes, this._formatKey));
return this.query('where', formatted);
}
// `fetch`. Now you can't call it on instances (see `renew`. It terminates a
// command chain and builds a query based on the state it accrued during its
// lifespan.
//
static fetch() {
let single = this._returnSingle,
columns = this._columns,
related = this._withRelated,
Model = this._ModelConstructor;
// Now construct query. We just discarded the last reference to the chain
// by not providing `query` with arguments.
let qb = this.chain().query();
if (single) qb.limit(1);
// NOTE: `_this._columns` is `undefined` by default, so becomes '*'.
return qb.select(columns)
.get('rows')
.map(record => Model.forge(_.mapKeys(record, this.parseKey))
.then((models) => _ModelConstructor.load(models, related))
}
static relation(relationName) {
// Ensure we have a relations object.
let relations = this._relations || this._relations = {};
// Do we already have a cached version of this relation?
if (relations[relationName]) {
// Yes, return it.
return relations[relationName];
} else if (this[relationName].__isRelation) {
// No, but we can make one.
return relations[relationName] = this[relationName]();
// Friendly errors.
} else if (this[relationName]) {
throw new Error(`
There is a static function called ${relationName}, but it has not
been marked as a relation with the `@relation` attribute.
`);
}
throw new Error(`Invalid relation name: ${relationName}`);
}
// -- Helpers.
static parseKey(key) {
return key;
}
static formatKey(key) {
return key;
}
static prefixKeys(attributes) {
return _.mapKeys(attributes, (value, key) => `${this.tableName}.key`)
}
} |
Okay, so now for some explanation... So, the important thing to note is that Babel's class Child extends Parent { //...
// is equivalent to:
Child.prototype = Parent.prototype;
Child.__proto__ = Parent; This means that every time you extend, you get to take your entire static interface with you. This is the first language I've been able to do this in, so pretty cool. Chain has all the static functions of // creating a new chain: internally `where` calls `Model.chain` which is the chain factory method.
// These are two separate instances:
let chain1 = Model.where();
let chain2 = Model.where();
// However, because we inherit the static interface, they are easily modified by custom class definitions.
@table('songs');
class Song extends Model {
static favourites() {
return this.query('listen_count', '>', 100); // calls `chain` internally.
}
}
@table('musicians');
class Music extends Model {
@relation
static songs() { return this.hasMany(Song); }
}
// Just works. (theoretically, ha)
Music.related('songs')
.favourites()
.whereIn('id', [0,1,2,3,4,5])
.query('artist_name', 'ilike', 'devo')
.query().count()
.then(result => console.log('you learned: ', result)); |
So, something I noticed is true in the current code base, is that So why bother instantiating these per object? You only need one per class. The basic idea is that the supplied relation definitions, eg @relation
static function acquaintances { return this.hasMany(Acquaintance).through(Friend); } ...are considered to be factories for Relation models. (Much as they are now, but this time attached to the constructor instead of the instance, and only executed once). The additional advantage of having these So, this makes these types of things much simpler: let person = {
name: 'Joe',
friends: [
{name: 'Tom'}, {name: 'Dick'}, {name: 'Harry'}
]
}
Person.withRelated('friends').save(person)
.then(joe => console.log('joe is friends with ', _.pluck(joe.toJSON().friends, 'name').join(', ')); This is how I do the same thing now: var person = // as above
var friends = person.friends;
Person.forge(_.omit(person, _.isObject)).save().tap(function(person) {
// Note constructing lots of Model instances here, would no longer be necessary.
return person.related('friends').add(friends).invokeThen('save');
}).then(function (joe) {
console.log('joe is friends with ', _.pluck(joe.toJSON().friends, 'name').join(', ');
}); Now |
But first I'll mention transactions. This is my proposed API: bookshelf.transaction(transacting => {
// `withRelated()` without arguments defaults to saving all known relations.
return Model(transacting).withRelated().save(data).then(model => { //...
// Or we can do the equivalent from a `model` instance (the old way) like so:
return Model.forge(data).save({withRelated: true, transacting}).then(model => { //...
}); Basically (shown in the massive |
It might be easier to create some of these as gists so we can discuss/update/fork with actual diffs, but a few notes:
I just pushed a branch |
Okay, stuff to think about certainly. I'll have a look through your branch.
|
Yep, in your example: User.where('id', 1).withRelated('posts.tags').fetch() wouldn't work, because the var db = Bookshelf(knex)
db(User).where('id', 1).withRelated(... or db('users').where(...
Columns/types/potentially indexes as well, and while this info might not be required there's a whole lot more you can do / optimize (and less you'd need to type) when you know everything about the data layer looks like upfront, though I don't think this info should be defined on the model... it should be passed into the initialization of the Bookshelf/Knex libs. In fact, you might not need™ to define "bookshelf models" unless you need to hook in for validation/serialization/instrumentation purposes, they could be created automatically based on the internal knowledge of the schema. I'd also like to ensure there's appropriate helpers in knex to fetch the schema info from the DB so you don't need to write them all out by hand if you'd prefer the DB itself be the source of the schema. |
Neither the constructor, not the models have a reference to {Model} = Bookshelf(knex);
@table('users')
User extends Model { ... }
// `User` has no knowledge of connection/knex
User.where('id', 1).withRelated('posts.tags').fetch().then(user => //...
|<----- knex instance lifespan ----->|
// `user` also doesn't have any knowledge of db or knex. It just knows it can
// call `super.save(this)` to save itself.
Well, you may not need to, but generally I like to add methods to my models for use within my app. My models tend to have a lot of business logic or helper methods to them. eg. The kinds model extensions I use (as a consumer of Bookshelf 0.8) can be grouped into three categories: instance methods ( What I would like to see is a clean, accessible way of defining and codifying those concepts and separating their APIs and state. But also allowing new models to be extended easily for use in application logic. The What I really don't want is model instances to have any knowledge of a connection, or a knex instance. Just to be simple data containers with patch({withRelated, transacting}) {
return this.constructor(transacting).withRelated(related).patch(this);
}
create({withRelated, transacting}) {
return this.constructor(transacting).withRelated(related).create(this);
}
// ... Sorry if I'm banging on about this, I just want to make sure that you understand what I'm getting at. I feel as though I haven't fully grasped your vision presently.
That's good to hear. I was under the impression that you didn't want that level of coupling between schema and ORM. Ultimately, I'd probably prefer to tie the schema to the models, rather than read the schema from the DB. Then use a tool to generate knex migrations. That seems pretty far off at this stage though. |
Yeah, this is what I'd like to get away from even further. Model should not be required to have knex in scope at construction time. You should be able to import {Model} from 'bookshelf' // not bookshelf(knex)
class User extends Model {
// ...
}
Yeah, for some, sure. I have an application with ~30ish tables and only about 4 of them need actual "business logic" methods defined... the rest of it is boilerplate that disappears when you know the schema. And yeah, it'd basically mean that any static methods would just be defined directly on prototype
Yeah that's sort of the idea, that you could potentially do both. |
Right. The one advantage I can really see to this is the ability to break models into different modules without having to pass a bookshelf instance to them. Is that the idea? In my current project I've separated my models and bookshelf initialization into a separate module, but this means doing this: # 'server' module
orm = require('models')(config)
MyModel = orm.model('MyModel')
# 'models' module
module.exports = (config) ->
knex = require('knex')(config.knex)
bookshelf = require('bookshelf')(knex)
getFiles('./models').forEach (file) ->
require(file)(bookshelf, config)
# ./models/*
module.exports (bookshelf) ->
bookshelf.model 'ModelName', bookshelf.Model.extend {
# ...
} It's kind annoying. It would be a lot cleaner if I could do this (which you appear to be suggesting): # 'my-models' module
module.exports {
User = require('./models/user')
Account = require('./models/account')
#...
}
# ./models/*
bookshelf = require('bookshelf')
module.exports = bookshelf.Model.extend {
# ...
}
# 'server' module
knex = knex(config)
bookshelf = bookshelf(knex)
bookshelf.registerModels(require('my-models')) That said, all bookshelf = function(modelName) {
Model = this.modelCache[modelName];
// In my approach like this. In yours you'd extend the prototype.
Model.createQueryBuilder = function() {
return new knex(Model.tableName);
}
}
bookshelf.registerModels = function(models) {
let cache = bookshelf.modelCache;
_.extend(cache, models);
} So it's really not a very big change. In fact, I wonder how hard it would be to get a change like this going in master presently...
I can certainly see the advantage to that approach. Implicit relations would be very cool, but they would still require names for use. For instance, table A could have two different has-many relations to table B, (ie. table B has two FK columns referencing table A). So this would be ambiguous: // Just assuming that you'd use table names instead of
// model names when they're implicitly defined.
bookshelf('table_a').related('table_b').fetchAll() Or perhaps that's fine to have as a kind of default setting, and you can upgrade to class definitions when you feel a concept is sufficiently complex, the above case being an example of that? |
It's a bit bigger than that, because there's a lot more that I'd like to accomplish with this change. Also mutating the model constructor like that sort of defeats the purpose, in mine you wouldn't extend the prototype, you'd instantiate a new Model passing the connection in the constructor.... no more using "new" Also if you have multiple connections accessing the same models, you can't mutate them, you'd need to extend and cache
Presumably the relations would come with pre-defined names. Haven't fully determined the syntax. |
Right. I'm with you now. I'll read through the
Thoughts on syntax: You could generate names based on column names, using similar assumptions that Bookshelf uses currently. So, say there is a table called So, If you've already called // entry point to program.
knex = require('knex')(/** config **/)
bookshelf = require('bookshelf')(knex);
// No definition required, bookshelf intuits (and caches) relation `friends` when it
// first initializes `Person` internally, via schema analysis.
bookshelf('Person').friends().where('name', 'ilike', 'Joe%').fetchAll()
// Now just say your 'friends' relation doesn't follow the required convention, you could do either of these:
{Model, HasMany} = bookshelf;
// cbf defining a model:
bookshelf('Person').addRelation(HasMany, 'friends', 'people', 'friendFK');
bookshelf('Person').friends().where('name', 'ilike', 'Joe%').fetchAll()
// or with a model (not necessarily using decorators and stuff, I'm not even sure
// if they're a good idea, but you get the point)
@tableName 'people'
Person extends Model {
sayHello() {
console.log(`Hello, ${@get('name')}!`);
}
// not sure if this attribute or static is necessary now, but via some method.
@relation
static friends() { return new HasMany(this, 'people', 'friendFK') };
}
// Register that model.
bookshelf.registerModel('Person', Person);
// Use original syntax. Only this time you get your decorated model.
bookshelf('Person').friends().where('name', 'ilike', 'Joe%').fetchAll()
// Can still access `familyMembers` because it *did* follow bookshelf conventions.
bookshelf('Person').familyMembers()... I realized while writing this that only one side of the relationship has a foreign key though, so it's not going to be able to generate names via the column name for everything (in fact the above example a self-referential edge case, it would usually only work for |
Also, I'm sure you're aware of this, but having the models unaware of connections unify the transaction and normal syntax, which is really cool. bookshelf('Model')...
bookshelf.transaction((transaction) {
transaction('Model')...
}); But also another reason for not storing the queryBuilder in the model instance itself, as it might be tempting to take the model instance and use it outside of the transaction cb (which would not work). Perhaps the result of bookshelf.transaction((t) {
return t('MyModel').where(...).fetch()
.then(m => t('MyModel').load(m, 'relationName')
.then(m => // ...
}.then(modelObject => {
// reusing model instance from trx, but it doesn't matter because all the connection info
// is coming from `bookshelf.
return bookshelf(`MyModel`).save(modelObject);
}) Hm... Certainly not as readable as |
Hey, after a comment on another issue I thought I'd raise my support for collections here rather confusing another thread... First off I've only been using bookshelf for about 2-3 weeks so please tell me if I'm missing something. I do however come from a backbone background of about 2-3 years. I find collections great for keeping models clean to handle instance specific tasks and collections great for handling multiple instance specific tasks.I don't have a collection for every model in my project, so I can see how they may be viewed as pointless in some applications but where I do have them I find them very useful. At the moment I'm mostly using collections to load a group of models with a given query or a cache for model's I've already fetched and then retrieving them using collection.find rather than having to implement this manually using an array. And in some places I'm using other underscore methods like filter which again saves implementing it manually. But some collections also have helper methods on them which save me re-writing the code or confusing the model. e.g. (if you need some context let me know)
That said, collections aren't perfect, for example I would argue that the model.fetchAll method and collection.fetchOne are very confusing/pointless as model.fetchAll is just collection.fetch and collection.fetchOne is just model.fetch. And there's no way to save everything in a collection (that I can find) which means you end up having to do something like
|
@rhys-vdw Is this conversation on-going somewhere? Is most of the work just being done on the |
Last public commit to |
@kevinob11 @jordansexton Bookshelf is not currently under active development. |
@kevinob11 @jordansexton I'd suggest objection - https://github.com/Vincit/objection.js/ It also plays nicley with es6 & 7. |
@rhys-vdw Abandoned? |
@arnold-almeida |
Hi @vellotis @rhys-vdw ! I'm wondering about the status of Bookshelf. I'm using it for a new project and I really like it. I saw some ongoing discussions (in 2015/2016) about the next version, and using ES6/7. Are there plans to continue development of Bookshelf or... should I consider moving to another ORM? |
@fcarreiro Bookshelf and Knex are stable, flexible, full featured, well tested, and used in production by many individuals and organizations. In addition, there are many plugins still being developed for it. By all means, consider Bookshelf (or any other ORMs you might evaluate) as you would any other library: on its merits. You're essentially asking the library maintainers to project their expected effort in the future, and thus bear responsibility for your decision to use (or not use) Bookshelf now. As a counterpoint, if Bookshelf currently seems unmaintained/is missing features/doesn't use ES6/7, and that presents a problem for your use of it right away, what would really cause that perception to change? Use Bookshelf, or "consider moving to another ORM" if you think that's best, but it seems to me that evaluating the historical and current development on the project per the commit history, Pulse, and Graphs on Github is a much better idea than deciding based on a reply to a comment on a 2 year old issue. Per @rhys-vdw's comment above, Bookshelf isn't under active development. To me, that's not a huge problem, because of all the existing great things about it, but you can read the above thread and look at the commit history to assess the state of the project. |
Hi @jordansexton, thanks for the answer! I have used Bookshelf, Sequelize and Waterline and also compared all the graphs you mention, and I ended up choosing Bookshelf. I think that it provides most of the things I need. My question was targeted at understanding if the current maintainers are planning to continue working on "new features/versions" or, for example, if they will mostly focus on bugfixes and minimal maintenance. In no way do I expect them to estimate their efforts nor do I expect them to choose an ORM for me. What could represent a problem for me is the amount of unprocessed issues and pull requests. I wanted to know that if I was to find some problem and/or to propose a pull request, that it would be taken into account. Also, that the dependencies are regularly updated, etc. I don't expect the maintainers to do all the work (for free). I'd like to contribute myself but I've had the experience in other projects that sometimes no matter how much effort you do, the project stays stale. Edit: regarding the stability, I certainly couldn't guess it from the versioning numbers, so I asked! ;) |
So I have been trying to think of ways that things could be improved. I've been using the library for about six months and I've hit many bugs and am aware of the weaker points of the API. Particularly the separation of concerns is problematic (especially when I was learning the interface).
I've never written any modern JS, but I've been trawling the docs after @tgriesser's example in #799. So I've taken the ideas I've had an think that they can be executed very effectively using the Babel toolset.
I've written up examples of how I think things could be implemented, and hopefully some of it resonates. Most of the changes are internal, so the API isn't drastically different.
Main things I want to address:
General separation of concerns
Query building and model state are too intertwined.
This, for instance, is very confusing to me:
Model.forge({a: 'a', b: 'b'}).where(...).fetchAll()
. I don't know what it does exactly, and would never write it, but I would prefer that it weren't possible.Also, having state around queries seems wrong. The presence of
resetQuery
makes me uneasy that I might accidentally leave state in a model without realising it.I would prefer very clear separation of concerns, where
Model
is a complex (but stateless) factory/resource management system, andmodel
is a very lightweight model with no concept of queries, and nowhere
.So none of these are legal anymore in my proposal:
Collections are not particularly useful
Pretty self explanatory and I think everyone agrees.
Queries are unexpected
Model.forge({id: 5}).someRelation().query()
Gives a query that is only constrained toModel#tableName
. This is confusing because you'd expect the library to be usingquery
internally as you chain functions. It's also problematic (issues aroundcount
, and pagination, and just general flexibility.)General refactor and modernization
The code is quite complex and would benefit from a refactor. Particularly in the relations area. In general I'd say that things are too stateful. There are also a few crufty abstractions like
Sync
that are very unclear how to use.No nested saving, no bulk inserts.
Not even that hard a problem, unless I'm missing something. The main issue is that models are not aware of their relations. @tgriesser's suggestion of ES7 decorators comes in handy here.😄
Too much configuration
The
options
convention is really useful, but seems bloated in some areas. Better to split this behaviour out into different well defined methods.Anyway, I'm waffling on because I'm about to post lots of code and I wanted to give it some context. Sorry for the immense read!
The text was updated successfully, but these errors were encountered: