How can I query in Bookshelf/Knex on a BelongsToMany join table? - mysql

I am running Bookshelf over MySQL in a Node application.
I have a model called Document and another called Tag, which are joined through a belongsToMany relationship via a table called "map_tag_document".
Document:
'use strict';
const bookshelf = require('../bootstrap/bookshelf_instance').bookshelf;
const Tag = require('./tag').model;
const Document = bookshelf.Model.extend({
tableName: 'document',
tags() {
return this.belongsToMany(Tag, 'map_tag_document', 'document_id', 'tag_id')
}
},
{
jsonColumns: ['data']
}
);
module.exports.model = Document;
Tag:
'use strict';
const bookshelf = require('../bootstrap/bookshelf_instance').bookshelf;
const Tag = bookshelf.Model.extend({
tableName: 'tag'
});
module.exports.model = Tag;
Tags have a "name" column.
How do I query documents based on a search string appearing in the names of the tags associated with them?
Currently, I'm querying like this:
await new Document()
.query((qb) => {
if (searchString)
qb.whereRaw(`(data->'$.description' LIKE "%${searchString}%" OR name LIKE "%${searchString}%")`)
})
.fetch({
withRelated: ['tags']
});
What's the proper syntax for querying on the joined table?

Figured it out-can do joins on the Knex querybuilder object, like this:
const documents = await new Document()
.query((qb) => {
qb.join('map_tag_document', 'document.id', '=', 'map_tag_document.document_id')
qb.join('tag', 'tag.id', '=', 'map_tag_document.tag_id')
if (searchString)
qb.andWhereRaw(`(document.data->'$.description' LIKE "%${searchString}%" OR document.name LIKE "%${searchString}%" OR tag.name LIKE "%${searchString}%")`)
})
.fetch({
withRelated: ['tags']
});

Related

Connecting 3 separate tables with a single through table in Sequelize

I'm trying to figure out why this is not working as intended. Any insights would be appreciated. Here's my situation:
I have a legacy connection of two tables with a through table. Something like:
Product model
const Product = sequelize.define('Product', {
// model attributes
});
Product.associate = (models) => {
Product.belongsToMany(models.Client, { through: models.ProductOwner, as: 'Owners'});
}
Client model
const Client = sequelize.define('Client', {
// model attributes
});
Client.associate = (models) => {
Client.belongsToMany(models.Product, { through: models.ProductOwner, as: 'OwnedProducts'});
}
ProductOwner
const ProductOwner = sequelize.define('Product', {
// no attributes
});
These form the N:M association with the through table ProductOwner.
This allows me, for example, to easily add a client to a product or getting all products that an existing client owns.
product.addOwner(client);
client.getOwnedProducts();
Now, I have the need to establish another chain of ownership to products that is unrelated to client. However, since this is still ownership, I would like to use the ProductOwner through table. So I add the new model:
Company model
const Company = sequelize.define('Company', {
// model attributes
});
Company.associate = (models) => {
Company.belongsToMany(models.Product, { through: models.ProductOwner, as: 'OwnedProducts'})
}
And the new association to the Products model. I also write a migration to add the CompanyId to the ProductOwners table and verify that the new reference is built into the database.
Product model
const Product = sequelize.define('Product', {
// model attributes
});
Product.associate = (models) => {
Product.belongsToMany(models.Client, { through: models.ProductOwner, as: 'Owners'});
Product.belongsToMany(models.Company, {
through: models.ProductOwner, as: 'CompanyOwners'
});
}
Now, on my code, I should be able to write:
product.addCompanyOwner(company);
company.getOwnedProducts();
And indeed, using the product instance method to add a new company does not throw any errors. However, the CompanyId column in the ProductOwners through table is still NULL.
Logging the query generated by Sequelize I see that the references to ProductId and ClientId are there, but there is no mention of CompanyId. Looks as if it is not recognizing that a new reference exists from Sequelize's point of view. However, the instance methods do work...
Which brings me to the question of why do they work? I assume that, by working, Sequelize is indeed creating the associations. But if that is the case, then why does the value for CompanyId is not set with the query?
Even writing it explicitly does not produce the expect result of setting CompanyId...
db.ProductOwner.create({
ProductId: 1,
ClientId: 1
}) // works to set all values
db.ProductOwner.create({
ProductId: 1,
CompanyId: 1
}) // sets ProductId to 1, but CompanyId is still NULL
What am I missing?

Too tidious hooks when querying in REST. Any ideas?

I've just started using feathers to build REST server. I need your help for querying tips. Document says
When used via REST URLs all query values are strings. Depending on the service the values in params.query might have to be converted to the right type in a before hook. (https://docs.feathersjs.com/api/databases/querying.html)
, which puzzles me. find({query: {value: 1} }) does mean value === "1" not value === 1 ? Here is example client side code which puzzles me:
const feathers = require('#feathersjs/feathers')
const fetch = require('node-fetch')
const restCli = require('#feathersjs/rest-client')
const rest = restCli('http://localhost:8888')
const app = feathers().configure(rest.fetch(fetch))
async function main () {
const Items = app.service('myitems')
await Items.create( {name:'one', value:1} )
//works fine. returns [ { name: 'one', value: 1, id: 0 } ]
console.log(await Items.find({query:{ name:"one" }}))
//wow! no data returned. []
console.log(await Items.find({query:{ value:1 }})) // []
}
main()
Server side code is here:
const express = require('#feathersjs/express')
const feathers = require('#feathersjs/feathers')
const memory = require('feathers-memory')
const app = express(feathers())
.configure(express.rest())
.use(express.json())
.use(express.errorHandler())
.use('myitems', memory())
app.listen(8888)
.on('listening',()=>console.log('listen on 8888'))
I've made hooks, which works all fine but it is too tidious and I think I missed something. Any ideas?
Hook code:
app.service('myitems').hooks({
before: { find: async (context) => {
const value = context.params.query.value
if (value) context.params.query.value = parseInt(value)
return context
}
}
})
This behaviour depends on the database and ORM you are using. Some that have a schema (like feathers-mongoose, feathers-sequelize and feathers-knex), will convert values like that automatically.
Feathers itself does not know about your data format and most adapters (like the feathers-memory you are using here) do a strict comparison so they will have to be converted. The usual way to deal with this is to create some reusable hooks (instead of one for each field) like this:
const queryToNumber = (...fields) => {
return context => {
const { params: { query = {} } } = context;
fields.forEach(field => {
const value = query[field];
if(value) {
query[field] = parseInt(value, 10)
}
});
}
}
app.service('myitems').hooks({
before: {
find: [
queryToNumber('age', 'value')
]
}
});
Or using something like JSON schema e.g. through the validateSchema common hook.

How to alter keys in immutable map?

I've a data structure like this (generated by normalizr):
const data = fromJS({
templates: {
"83E51B08-5F55-4FA2-A2A0-99744AE7AAD3":
{"uuid": "83E51B08-5F55-4FA2-A2A0-99744AE7AAD3", test: "bla"},
"F16FB07B-EF7C-440C-9C21-F331FCA93439":
{"uuid": "F16FB07B-EF7C-440C-9C21-F331FCA93439", test: "bla"}
}
})
Now I try to figure out how to replace the UUIDs in both the key and the value of the template entries. Basically how can I archive the following output:
const data = fromJS({
templates: {
"DBB0B4B0-565A-4066-88D3-3284803E0FD2":
{"uuid": "DBB0B4B0-565A-4066-88D3-3284803E0FD2", test: "bla"},
"D44FA349-048E-4006-A545-DBF49B1FA5AF":
{"uuid": "D44FA349-048E-4006-A545-DBF49B1FA5AF", test: "bla"}
}
})
A good candidate seems to me the .mapEntries() method, but I'm struggling on how to use it ...
// this don't work ... :-(
const result = data.mapEntries((k, v) => {
const newUUID = uuid.v4()
return (newUUID, v.set('uuid', newUUID))
})
Maybe someone can give me a hand here?
mapEntries is the correct method. From the documentation, the mapping function has the following signature:
mapper: (entry: [K, V], index: number, iter: this) => [KM, VM]
This means that the first argument is the entry passed in as an array of [key, value]. Similarly, the return value of the mapper function should be an array of the new key and the new value. So your mapper function needs to look like this:
([k, v]) => {
const newUUID = uuid.v4()
return [newUUID, v.set('uuid', newUUID)]
}
This is equivalent to the following (more explicit) function:
(entry) => {
const key = entry[0]; // note that key isn't actually used, so this isn't necessary
const value = entry[1];
const newUUID = uuid.v4()
return [newUUID, value.set('uuid', newUUID)]
}
One thing to note is that the templates are nested under the templates property, so you can't map data directly -- instead you'll want to use the update function.
data.update('templates', templates => template.mapEntries(...)))
So putting everything together, your solution should look like the following:
const result = data.update('templates', templates =>
templates.mapEntries(([k, v]) => {
const newUUID = uuid.v4()
return [newUUID, v.set('uuid', newUUID)]
})
);

BookShelf orm MySQL how to select column1-column2 as alias

In a raw MySQL query, I have something like this:
Select total_sales - over_head_costs As net_sales from departments;
How can I realize the same thing with BookShelf /knex query? Ideally not using knex.raw.
My attempt involves following:
let Department = bookshelf.Model.extend({
tableName: 'departments',
idAttribute: 'department_id',
},{
getDepartments: function(){
return this.fetchAll({columns: ['department_id', 'department_name', 'over_head_costs', 'total_sales - over_head_costs AS net_sales']})
.then(models=>models.toJSON());
},
});
Bookshelf does not have this feature but it brings a plugin for that: Virtuals
. No need to install anything, you just load it right after loading Bookshelf using bookshelf.plugin('virtuals').
Your model should then look like:
const Department = bookshelf.Model.extend({
tableName: 'departments',
idAttribute: 'department_id',
virtuals: {
net_sales: function() {
return this.get('total_sales') - this.get('over_head_costs');
}
}
},{
getDepartments: function(){
return this.fetchAll({columns: ['department_id', 'department_name', 'over_head_costs', 'net_sales']})
.then(models=>models.toJSON());
},
});

Working with JSON data.. why are all properties of my object undefined?

I am attempting to take the Name and ID fields from each object, but the fields are appearing undefined.
function OnHistoricalListBoxLoad(historicalListBox) {
$.getJSON('GetHistoricalReports', function (data) {
historicalListBox.trackChanges();
$.each(data, function () {
var listBoxItem = new Telerik.Web.UI.RadListBoxItem();
listBoxItem.set_text(this.Name);
listBoxItem.set_value(this.ID);
historicalListBox.get_items().add(listBoxItem);
});
historicalListBox.commitChanges();
});
}
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult GetHistoricalReports()
{
List<HistoricalReport> historicalReports = DashboardSessionRepository.Instance.HistoricalReports;
var viewModel = historicalReports.Select(report => new
{
ID = report.ID,
Name = report.Name
}).ToArray();
return Json(viewModel, JsonRequestBehavior.AllowGet);
}
I know that I am returning the data successfully, and I know that there is valid data. I am new to MVC/JavaScript, though.. I checked case sensitivity to ensure that I wasn't making just an easy mistake, but it does not seem to be the issue. Am I missing something more complex?
Inspecting the HTTP Response JSON tab in Chrome I see:
0: {ID:1, Name:PUE}
1: {ID:2, Name:Weight}
2: {ID:3, Name:Power Actual vs Max}
3: {ID:4, Name:Power Actual}
No idea, but passing such behemoth domain models to views is very bad practice. This is so kinda domain polluted that it has nothing to do in a view. In a view you work with views models. View models contain only what a view needs. In this case your view needs an ID and a Name. So pass a view model with only those single simple properties to this view:
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult GetHistoricalReports()
{
var reports = DashboardSessionRepository.Instance.HistoricalReports;
var reportsViewModel = reports.Select(x => new
{
ID = x.ID,
Name = x.Name
}).ToArray();
return Json(reportsViewModel, JsonRequestBehavior.AllowGet);
}
Now, not only that you will save bandwidth, but you will get some clean JSON:
[ { ID: 1, Name: 'Foo' }, { ID: 2, Name: 'Bar' }, ... ]
through which you will be able to loop using $.each.
UPDATE:
Now that you have shown your JSON data it seems that there is a Content property which represents the collection. So you need to loop through it:
$.each(data.Content, ...);
and if you follow my advice about the view models your controller action would become like this:
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult GetHistoricalReports()
{
var report = DashboardSessionRepository.Instance.HistoricalReports;
var reportsViewModel = report.Content.Select(x => new
{
ID = x.ID,
Name = x.Name
}).ToArray();
return Json(reportsViewModel, JsonRequestBehavior.AllowGet);
}
and now loop directly through the returned collection:
$.each(data, ...);