Joining a table through another join table using withRelated - bookshelf.js

I have 3 tables:
Order: id
Item: id
OrderItems: order_id, item_id
I want to be able to fetch a particular Order and get all the Items related to that Order, without the clutter introduced by doing Order.where(...).fetch({withRelated: ['orderItem.item']).
Ideally I'd like to do Order.where(...).fetch({withRelated: ['items']) and the items relationship knows to go through the OrderItems table to get the information.
I have tried the relationship
items() {
return this.hasMany('Item').through('OrderItem')
}
but that doesn't seem to work as I'd expected.
Is this possible using Bookshelf's API without writing a manual join?

The relationship you tried:
items() {
return this.hasMany('Item').through('OrderItem')
}
Means to Bookshelf:
Item(id, OrderItem_id) -> OrderItem(id, Order_id) -> Order(id)
Or a 1:n:m relationship.
But your database says:
Item(id) <- OrderItem(Item_id, Order_id) -> Order(id)
This kind of relationship on Bookshelf maps better as belongsToMany():
bookshelf.plugin('registry') // to ease the cyclic mapping
const Item = bookshelf.Model.extend({
tableName: 'item',
orders: function() {
this.belongsToMany('Order', 'OrderItem')
},
})
bookshelf.model('Item', Item)
const Order = bookshelf.Model.extend({
tableName: 'order',
items: function() {
//return this.hasMany('Item').through('OrderItem')
return this.belongsToMany('Item', 'OrderItem')
},
})
bookshelf.model('Order', Order)
Note that until you start placing useful data on the intermediate table you don't need to create a model for it.
With that a Order.where(...).fetch({withRelated: 'items'}).then(...) query should work as expected.

Related

How to paginate results when filtering on left join table column

Let's define for example these entities:
#Entity()
export class Author {
#OneToMany(() => Book, book => book.author)
books = new Collection<Book>(this);
}
#Entity()
export class Book {
#Property()
name: string;
#ManyToOne()
author: Author;
}
const authors = await authorRepository.find({ books: {name: 'Test'} }, {
limit: 10
});
As you can see i want to select all authors that have books with name 'Test', but this will generate the following query:
select `e0`.* from `author` as `e0`
left join `book` as `e1` on `e0`.`id` = `e1`.`author_id`
where `e1`.`name` = 'Test' limit 10
The problem is when i have more than 2 authors and each of them has more than 10 books with name 'Test', this query will return only the first author because of the limit clause.
I am not sure if this is a bug in the ORM or it's a expected behavior.
One way to fix that is to select all rows without the limit clause and do the pagination in memory like they are doing in hibernate, but i am not sure how much memory will be used with very large tables and this might block the event loop in NodeJS while processing them.
You could fallback to query builder here to apply group by clause:
const qb = await authorRepository.createQueryBuilder('a');
qb.select('a.*').where({ books: { name: 'Test' } }).limit(10).groupBy('a.id');
const authors = await qb.getResult();
Will think about how to support this use case directly via EM/repository API.

bookshelf.js recursive query plus ordering

I've found one example of recursive tree fetch but ordering does not work. What is the best way to fetch all items recursively with ordering at each level?
Model
knex.schema.createTable('items', function (table) {
table.increments();
table.string('title');
table.integer('parent');
table.integer('order');
});
var Item = bookshelf.Model.extend({
tableName: 'items',
items: function() {
return this.hasMany(Item, 'parent');
}
});
Recursive but only to certain amount of level, all would be preferred instead of specifying depth. This works but doesn't order related items correctly. Reference
Item.query({where: {parent: null}, orderBy: 'id'})
.fetchAll(
{
withRelated: ['items.items.items.items.items'],
orderBy: 'id'
});
Edit
I'm updating this with a recursive query that works for SQLite (note I'm using id for ordering because my bad choice of column name). Solution maybe to use bookshelf for crud operations and a raw query to populate objects that I need.
with recursive tc( i )
as ( select id FROM items WHERE parent IS NULL
UNION SELECT id FROM items, tc WHERE items.parent = tc.i ORDER BY id
)
select * FROM items WHERE id IN tc;

About normalize in redux real-world example

In the example's src(UserPage.js):
const mapStateToProps = (state, ownProps) => {
// We need to lower case the login due to the way GitHub's API behaves.
// Have a look at ../middleware/api.js for more details.
const login = ownProps.params.login.toLowerCase()
const {
pagination: { starredByUser },
entities: { users, repos }
} = state
const starredPagination = starredByUser[login] || { ids: [] }
const starredRepos = starredPagination.ids.map(id => repos[id])
const starredRepoOwners = starredRepos.map(repo => users[repo.owner])
return {
login,
starredRepos,
starredRepoOwners,
starredPagination,
user: users[login]
}
}
I notice that there is many templates like xxx.ids.map(id => someEntities[id]),I am not sure why use this pattern to work.IMO,I would use something like import { map } from 'lodash'; someList && map(someList, item => {...}) in the container component and just pass the entities in the mapStateToProps.
So,could someone explains it's purpose?Thanks.
The standard suggestion for normalizing data in Redux is to store data items in an object, with IDs as the keys and the items as the values. However, an object doesn't have an inherent order to it. (Technically, the order of iteration for object keys should be consistent, but it's a bad practice to rely on that as the sole means of ordering.)
Because of that, it's also standard to store an array of just the IDs as well. A typical example might look like:
{
byId : {
qwerty : { },
abcd : { },
aj42913 : { }
},
items : ["qwerty", "aj42913", "abcd"],
sorted : ["abcd", "aj42913", "qwerty"],
selected : ["qwerty", "abcd"]
}
In this example, items contains all item IDs, probably in the order they were insert. sorted contains the IDs in some sort of sorted order, while selected contains a subset of the IDs.
This allows the items themselves to only be stored once, while multiple representations of those items can be saved using various arrays of IDs.
From there, you can pull together a list of the actual items by mapping over whatever array of IDs you care about, and retrieving the items by their IDs.
So, ultimately the answer is that relying just on the keys of the byId object doesn't give you any kind of ordering, and doesn't allow defining subsets of the data.

Bookshelf relation with through and custom key columns

This is my relation chain.
applications.applicant_id -> users.id -> skills_users.user_id
skills_users.id <- ratings.rateable_id
I want get an Application with a custom relation applicant_skills, and preferably join in ratings. So I was hoping something like this would work:
applicant_ratings: function () {
return this.belongsToMany('SkillUser').through('User')
},
But I cannot get it to work. I have tried different combinations of "key"-parameters, but none seem to work for me.
The issue is that 'users' is not a pivot, but 'skills_users' is. More like:
applications.applicant_id -> users.id <- skills_users.user_id
skills_users.id <- ratings.rateable_id
Try perhaps:
var Application = bookshelf.Model.extend({
tableName: 'applications',
applicant_ratings: function () {
return this.belongsTo('User', 'applicant_id')
.hasMany('Rating')
.through('SkillUser','rateable_id');
}
});

Transforming on excluded field in meteor Mongo Collection

Meteor.publish("thing", function(options) {
return Collection.find({}, {fields: {anArray: 0}})
})
I exclude "anArray" because it contains userids not meant to be seen by each user. However it could contain the logged in user itself, in which case the user needs to know it.
Collection = new Mongo.Collection("thing", {
transform: function(document) {
_.each(document.anArray, function(item) {
item = true
})
return document
}
})
Above I try to transform the collection(simplified) but because the "anArray" is excluded, "anArray" is simply not defined.
How can I let the user know he is in "anArray" without compromising all other users in "anArray"? (I tried to do that in the transform.)
You can use the package I developed, meteor-middleware. It provides a nice pluggable API for this. So instead of just providing a transform, you can stack them one on another. This allows for code reuse, permissions checks (like removing or aggregating fields based on permissions), etc.
For example, for your particular problem, you could do (in CoffeeScript):
thing = new PublishEndpoint 'thing', (options) ->
Collection.find {}
class HideAnArrayMiddleware
added: (publish, collection, id, fields) =>
fields.anArray = _.intersection fields.anArray, [publish.userId] if fields.anArray
publish.added collection, id, fields
changed: (publish, collection, id, fields) =>
fields.anArray = _.intersection fields.anArray, [publish.userId] if fields.anArray
publish.changed collection, id, fields
thing.use new HideAnArrayMiddleware()
As described in this answer, here is how you access document fields before publishing them:
// server: publish the rooms collection
Meteor.publish("rooms", function () {
var self = this;
var handle = Rooms.find({}).observeChanges({
added: function(id, fields) { self.added("rooms", id, fields); },
changed: function(id, fields) { self.changed("rooms", id, fields); },
removed: function(id) { self.added("rooms", id); },
}
});
self.ready();
self.onStop(function () { handle.stop(); });
});
In your case, maybe you can do something like this:
added: function(id, fields) {
if (fields.anArray)
if (fields.anArray.indexOf(self.userId) !== -1)
fields.anArray = [self.userId];
else
delete fields.anArray;
self.added("rooms", id, fields);
},
You'll also have to take care of the changed function in a similar way.
It's not possible to include or exclude elements of an array, so your best bet is to define an explicit Boolean field in the document for the user being in the array.
Also, because transforms on the server are ignored (please vote for this issue), you'll have to set that field in the database if it's dynamically computed. Similar SO questions: 1, 2, 3.
An alternative is to define a non-database-backed collection. Have a look at the counts-by-room example.

Resources