Query on related data using Bookshelf.js - node.js

Does Bookshelf.js provide a method to find a model by it's related model?
I have create the two models Order and OrderStatus. Now, I want to fetch an Order model by it's status. Unfortunately, this approach isn't working anymore, as the load method expects a string or string array now.
const order = await new Order().load({"orderStatus": q => q.where({"userId": userId, "status": 10})});
I've found a library called "bookshelf-eloquent" that adds this functionality. However, I'm using Typescript and this library doesn't provide any type declaration.
This code works, but TypeScript indicates that the property whereHas doesn't exist.
const order = await new Order()
.whereHas("orderStatus", q => q.where({"userId": userId, "status": 10}))
.get()
Either the Bookshelf.js developers have added a new method I haven't seen yet, or I need to have a type declaration file for the bookshelf-eloquent library. Otherwise, I can't use it.

Related

Why we pass document ID to mongoose.Types.ObjectId(documentId) instead of directly passing string id

So when creating or finding (based on reference) documents in mongodb using mongoose we use two methods to pass object id. One is simply passing the id in form of string and the other is by the mongoose.Types.ObjectId(documentId). What's the difference between them?
For better understanding see these two examples:
Option 1:
const articles = await Article.find({user: userId})
Option 2:
const articles = await Article.find({user: mongoose.Types.ObjectId(userId)}
So, do we really need to first pass it to mongoose.Types.ObjectId?
PS: I know that passing to the mongoose.types.objectId will throw an error in case if id is not an object id of MongoDB. I'm seeking if there is any other benefit of this approach or not.

How to reuse graphQl resolvers

My project based on Node, GraphQl (apollo), MongoDb.
I have basic entity Product:
type Product {
id: ObjectId
name: String
bestPrice: Number
annualSavings: Number
...
}
bestPrice and annualSavings are calculated in base on separate resolvers:
const bestPriceResolver = (product, args, context) => {
return getBestPrice(context.dataStore)
}
const annualSavingsResolver = (product, args, context) => {
const bestPrice = getBestPrice(context.dataStore);
return getAnnualSavings = getAnnualSavings(bestPrice);
}
composeResolvers ({
bestPrice: bestPriceResolver,
annualSavings: annualSavingsResolver,
})
Each resolver goes to database and fetch data for bestPrice. And with this approach I go to dadabase twice to fetch same data (bestPrice). Is it possible to do it once?
Yes, by using dataloader (https://github.com/graphql/dataloader). It's a generic utility which provides batching of requests (and also caching per request).
Throughout request you can collect all ids of some entity that you need, and then you can execute only one query with provided ids.
General idea is that you create new instance of loader for each request and store it in context. Then, instead of calling getBestPrice, you call loader's .load() or .prime() method, which basically collects all ids throughout request. Dataloader returns new function, which should be similar to getBestPrice, but instead of making one query per id, you'll need to provide an array of ids in query, and map responses according to order of ids provided. Dataloader will do the rest :)
Dataloader helps a lot with GraphQL optimizations, since GraphQL queries can often be very heavy.
YouTube provides tons of useful tutorials.

mongoose - select specific fields in Model.create

const generatedEvent = await Event.create(req.body);
res.send(generatedEvent);
I am getting some data from the request body and I can generate a new Event. And I'm returning the event to client when it has generated. But I don't want to return all fields with event. I want to make filter operation like how we are using select function like this: Event.find().select({title:1,description:1})
How can i use this select func with Model.create?
If you take a look at the mongoose-source code, you can see that Model.create returns a promise with the created/inserted documents. There's no way to specify a filtering-options to return only specific fields.
Of course you could do a .find() in combination with a .select() call after creating/inserting a new record but that would result in one extra DB-query for each insert which does not make a lot of sense.
You could instead just return the desired properties from the returned document, since you know that a new document was inserted successfully with the provided data, when the promise resolved. So you could simply do:
res.send({title: generatedEvent.title, description: generatedEvent.description});
Model.create() internally doesn't fetch the document from the database, rather it actually returns the result whether it's inserted successfully or not. If successful, mongoose will return the original mongoose document that mongoose created before sending to the database.
So you could just select the fields by yourself. Using es2015 Object destructuring assignment and Object shorthand property names would help writing more concise code.
const { title, description } = await Event.create(req.body); // Object destructuring
res.send({ title, description }); // Object shorthand property names

Google Datastore can't update an entity

I'm having issues retrieving an entity from Google Datastore. Here's my code:
async function pushTaskIdToCurrentSession(taskId){
console.log(`Attempting to add ${taskId} to current Session: ${cloudDataStoreCurrentSession}`);
const transaction = datastore.transaction();
const taskKey = datastore.key(['Session', cloudDataStoreCurrentSession]);
try {
await transaction.run();
const [task] = await transaction.get(taskKey);
let sessionTasks = task.session_tasks;
sessionTasks.push(taskId);
task.session_tasks = sessionTasks;
transaction.save({
key: taskKey,
data: task,
});
transaction.commit();
console.log(`Task ${taskId} added to current Session successfully.`);
} catch (err) {
console.error('ERROR:', err);
transaction.rollback();
}
}
taskId is a string id of another entity that I want to store in an array of a property called session_tasks.
But it doesn't get that far. After this line:
const [task] = await transaction.get(taskKey);
The error is that task is undefined:
ERROR: TypeError: Cannot read property 'session_tasks' of undefined
at pushTaskIdToCurrentSession
Anything immediately obvious from this code?
UPDATE:
Using this instead:
const task = await transaction.get(taskKey).catch(console.error);
Gets me a task object, but it seems to be creating a new entity on the datastore:
I also get this error:
(node:19936) UnhandledPromiseRejectionWarning: Error: Unsupported field value, undefined, was provided.
at Object.encodeValue (/Users/.../node_modules/#google-cloud/datastore/build/src/entity.js:387:15)
This suggests the array is unsupported?
The issue here is that Datastore supports two kinds of IDs.
IDs that start with name= are custom IDs. And they are treated as strings
IDs that start with id= are numeric auto-generated IDs and are treated as integers
When you tried to updated the value in the Datastore, the cloudDataStoreCurrentSession was treated as a string. Since Datastore couldn't find an already created entity key with that custom name, it created it and added name= to specify that it is a custom name. So you have to pass cloudDataStoreCurrentSession as integer to save the data properly.
If I understand correctly, you are trying to load an Array List of Strings from Datastore, using a specific Entity Kind and Entity Key. Then you add one more Task and updated the value of the Datastore for the specific Entity Kind and Entity Key.
I have create the same case scenario as yours and done a little bit of coding myself. In this GitHub code you will find my example that does the following:
Goes to Datastore Entity Kind Session.
Retrieves all the data from Entity Key id=5639456635748352 (e.g.).
Get's the Array List from key: session_tasks.
Adds the new task that passed from the function's arguments.
Performs the transaction to Datastore and updates the values.
All steps are logged in the code and there are a lot of comments explaining exactly how the code works. Also there are two examples of currentSessionID. One for custom names and other one for automatically generated IDs. You can test the code to understand the usage of it and modify it according to your needs.

Bookshelf.JS: Related Model is trying to be used before being required

I have two models that are related, Customers and Addresses. I first discovered this issue when I was trying to create a customer with a related address. For our purposes, a single customer can have multiple addresses, and when creating a new customer, we want to create an address at the same time as we create the customer.
I did some digging through the documentation and set up the relationship as best as I could, and this seemed to work well enough, but then I noticed that when I included both the models in modules together, (i.e. my routes/controllers), I was getting circular references.
Long story short, my research lead me to add the registry plugin to my bookshelf.js file. This worked at the time, but now it looks like my Address model isn't properly exported when being referenced in Customers.
Here's a snippet of my current configuration
// bookshelf.js
const bookshelf = require('bookshelf')(knex);
bookshelf.plugin([
'registry',
]);
module.exports = bookshelf;
// customers.js
const bookshelf = require('../bookshelf');
const Address = require('./address');
const Customer = bookshelf.Model.extend({
tableName: 'customers',
addresses: function () {
return this.hasMany('Address');
},
}, {
customCreate: function (attributes) {
return this.forge(attributes)
.save()
.tap(c => {
return Address.forge(attributes)
.save({
customer_id: c.get('id'),
});
})
}
});
module.exports = bookshelf.model('Customer', Customer);
// address.js
const bookshelf = require('../bookshelf');
const Customer = require('./customer');
const Address = bookshelf.Model.extend({
tableName: 'addresses',
customer: function () {
return this.belongsTo('Customer');
}
});
module.exports = bookshelf.model('Address', Address);
I started to notice that when I would run Customer.customCreate(), I got an error saying Address.forge is not a function. I threw some console logs into my customer.js file and saw that Address is an empty object ({}) when being referenced within customer.js. However, in other places, it's returning the proper Bookshelf model.
Looks to me like I'm trying to use my Address model in customers before it's properly required, which made me wonder if I'm structuring my project and models properly, or if there's any changes I need to make.
There's a circular reference problem alright. The best way to structure your models so that there are no such problems is to load them all during your app's initialization in a single file, e.g. index.js on your models' directory, attach each one to an object and export that object. Then you just require() that file and get access to all the models in a single place.
However, to solve your problem in a much easier way you just need to make a single change to your customCreate() method:
customCreate: function (attributes) {
return this.forge(attributes)
.save()
.tap(c => this.related('addresses').create(attributes))
}
}
This makes use of the Collection.create method to easily create a new model inside a collection, and since it's used on a relation it will also set the correct foreign key.
Note that the Registry plugin will not save you from circular dependency problems, but it will allow you to write your models in a way that avoids them.

Resources