withRelated binding is undefined - bookshelf.js

I have two model:
var Book = bookshelf.Model.extend({
tableName: 'books',
chapters: function(){
var chapters = require('./chapter').Chapter;
return this.hasMany(chapters, 'bookId');
}
});
module.exports = {
Book: Book
};
var Chapter = bookshelf.Model.extend({
tableName: 'chapters',
hasTimestamps: ['dateCreated', 'dateUpdated'],
//relationship
book: function(){
var book = require('./user').Book;
return this.belongsTo(book, 'bookId');
}
});
module.exports = {
Chapter: Chapter
}
I do this in one of the controller:
new Book.Book()
.fetch({withRelated:['chapters']})
.then(function(books){
resolve(books);
}).catch(function(err){
reject(err);
});
The "chapters": [] because the debug log give me this, the bindings is undefined:
{ method: 'select',
options: {},
bindings: [ 1 ],
sql: 'select `books`.* from `books` limit ?' }
{ method: 'select',
options: {},
bindings: [ undefined ],
sql: 'select `chapters`.* from `chapters` where `chapters`.`bookId` in (?)' }

Bookshelf assumes that you have a column called 'id' in each table which uniquely identifies each row. If you are using a primary key which is not called 'id' you need to specify what it is called to be able to use the model in a relation. This is done by the 'idAttribute' member e.g.
var ModelA = bookshelf.Model.extend({
tableName: "ModelA",
idAttribute: "modelA_id"
...
}
});
http://bookshelfjs.org/#Model-instance-idAttribute

Solved it with idAttribute: 'cardId' specified.

Related

How to dynamically update an attribute in a dynamodb item?

I created an item in dynamodb using Node js, the item has multiple attributes such as brand, category, discount, validity, etc. I am using uuid to generate ids for each item. Now let's say I want to update the validity attribute of the item, in which case I am currently sending the entire json object with the value of validity modified to the new value.
This is definitely not optimal, please help me find an optimal solution.
const params = {
TableName: process.env.PRODUCT_TABLE,
Key: {
id: event.pathParameters.id,
},
ExpressionAttributeNames: {
'#discount': 'discount',
},
ExpressionAttributeValues: {
':brand': data.brand,
':category': data.category,
':discount': data.discount,
':denominations': data.denominations,
":validity": data.validity,
":redemption": data.redemption
},
UpdateExpression: 'SET #discount = :discount, denominations = :denominations, brand = :brand, category = :category, validity = :validity, redemption = :redemption',
ReturnValues: 'ALL_NEW',
};
I want to send just the attribute I want to update with the new value, if I want to change the validity from 6 months to 8 months, I should just send something like:
{
"validity": "8 months"
}
And it should update the validity attribute of the item.
Same should apply to any other attribute of the item.
'use strict';
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.update = (event, context, callback) => {
const data = JSON.parse(event.body);
let attr = {};
let nameobj = {};
let exp = 'SET #';
let arr = Object.keys(data);
let attrname = {};
arr.map((key) => {attr[`:${key}`]=data[key]});
arr.map((key) => {
exp += `${key} = :${key}, `
});
arr.map((key) => {nameobj[`#${key}`]=data[key]});
attrname = {
[Object.keys(nameobj)[0]] : nameobj[Object.keys(nameobj)[0]]
}
const params = {
TableName: process.env.PRODUCT_TABLE,
Key: {
id: event.pathParameters.id,
},
ExpressionAttributeNames: attrname,
ExpressionAttributeValues: attr,
UpdateExpression: exp,
ReturnValues: 'ALL_NEW',
};
// update the todo in the database
dynamoDb.update(params, (error, result) => {
// handle potential errors
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501,
headers: { 'Content-Type': 'text/plain' },
body: 'Couldn\'t update the card',
});
return;
}
// create a response
const response = {
statusCode: 200,
body: JSON.stringify(result.Attributes),
};
callback(null, response);
});
};
Contrary to others comments, this is very possible, use the UpdateItem action.
Language agnostic API docs
JavaScript specific API docs
If you want to dynamically create the query, try something like this:
const generateUpdateQuery = (fields) => {
let exp = {
UpdateExpression: 'set',
ExpressionAttributeNames: {},
ExpressionAttributeValues: {}
}
Object.entries(fields).forEach(([key, item]) => {
exp.UpdateExpression += ` #${key} = :${key},`;
exp.ExpressionAttributeNames[`#${key}`] = key;
exp.ExpressionAttributeValues[`:${key}`] = item
})
exp.UpdateExpression = exp.UpdateExpression.slice(0, -1);
return exp
}
let data = {
'field' : { 'subfield': 123 },
'other': '456'
}
let expression = generateUpdateQuery(data)
let params = {
// Key, Table, etc..
...expression
}
console.log(params)
Output:
{
UpdateExpression: 'set #field = :field, #other = :other',
ExpressionAttributeNames: {
'#field': 'field',
'#other': 'other'
},
ExpressionAttributeValues: {
':field': {
'subfield': 123
},
':other': '456'
}
}
Using Javascript SDK V3:
Import from the right package:
import { DynamoDBClient PutItemCommandInput, UpdateItemCommandInput, UpdateItemCommand } from '#aws-sdk/client-dynamodb';
Function to dynamically do partial updates to the item:
(the code below is typescript can be easily converted to Javascript, just remove the types!)
function updateItem(id: string, item: any) {
const dbClient = new DynamoDBClient({region: 'your-region-here });
let exp = 'set ';
let attNames: any = { };
let attVal: any = { };
for(const attribute in item) {
const valKey = `:${attribute}`;
attNames[`#${attribute}`] = attribute;
exp += `#${attribute} = ${valKey}, `;
const val = item[attribute];
attVal[valKey] = { [getDynamoType(val)]: val };
}
exp = exp.substring(0, exp.length - 2);
const params: UpdateItemCommandInput = {
TableName: 'your-table-name-here',
Key: { id: { S: id } },
UpdateExpression: exp,
ExpressionAttributeValues: attVal,
ExpressionAttributeNames: attNames,
ReturnValues: 'ALL_NEW',
};
try {
console.debug('writing to db: ', params);
const command = new UpdateItemCommand(params);
const res = await dbClient.send(command);
console.debug('db res: ', res);
return true;
} catch (err) {
console.error('error writing to dynamoDB: ', err);
return false;
}
}
And to use it (we can do partial updates as well):
updateItem('some-unique-id', { name: 'some-attributes' });
What i did is create a helper class.
Here is a simple function : Add all the attribute and values that goes into, if the value is null or undefined it won't be in the expression.
I recommande to create a helper class with typescript and add more functions and other stuff like generator of expressionAttributeValues , expressionAttributeNames ... , Hope this help.
function updateExpression(attributes, values) {
const expression = attributes.reduce((res, attribute, index) => {
if (values[index]) {
res += ` #${attribute}=:${attribute},`;
}
return res;
}, "SET ");
return expression.slice(0, expression.length - 1)
}
console.log(
updateExpression(["id", "age", "power"], ["e8a8da9a-fab0-55ba-bae3-6392e1ebf624", 28, undefined])
);
You can use code and generate the params object based on the object you provide. It's just a JavaScript object, you walk through the items so that the update expression only contains the fields you have provided.
This is not really a DynamoDB question in that this is more a general JS coding question.
You can use UpdateItem; to familiarize yourself with DynamoDb queries I would suggest you DynamoDb NoSQL workbench:
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.settingup.html
It can generate snippets for you based on your queries.
DynamoDb NoSQL workbench screenshot query

1-1 relation load with bookshelf

I have 2 tables something likes;
Property {id, name, address_id}
Address {id, city, country, add_line_1, ...}
I linked Property to Address via address_id.
How can I config Bookshelf model to load Property with associated Address via address_id.
I'm trying something likes;
PROPERTY = bookshelf.Model.extend({
tableName: 'PROPERTY',
address: function() {
return this.hasOne(ADDRESS, 'address_id');
}
});
ADDRESS = bookshelf.Model.extend({
tableName: 'ADDRESS',
property: function() {
return this.belongsTo(PROPERTY);
}
});
The error: "Unknown ADDRESS.address_id in where clause"
From the documentation the hasOne() expects the foreign key to be ON THE OTHER table. So try changing your models to:
PROPERTY = bookshelf.Model.extend({
tableName: 'PROPERTY',
address: function() {
return this.belongsTo(ADDRESS, 'address_id');
}
});
ADDRESS = bookshelf.Model.extend({
tableName: 'ADDRESS',
property: function() {
return this.hasOne(PROPERTY);
}
});
And query using, by example:
PROPERTY.fetchAll({withRelated:'address'}).then((a)=>
{console.log(a.toJSON())});

How to get specific columns from join table using withRelated bookshelf

for example i got 2 tables like;
table1 {Id, Name, Description}
table2 {Id, Table1Id, Name, Amount}
With bookshelfJS when i using withRelated something like;
new table1({Id: 1})
.fetchAll({
withRelated: ['Childs']})
.then(function(rows) {
callback(null, rows);
});
I expected my result something like;
{results: [{Id: '', Name: '', Description: '', Childs: [{Id: '', Name: '', Amount: 123}]}]}
I don't want to get Table1Id in the Childs list. How can I specify what columns in my output?
UPDATE
My models;
table1 = bookshelf.Model.extend({
tableName: 'table1',
Childs: function() {
return this.hasMany(table2, 'Table1Id');
}
});
table2 = bookshelf.Model.extend({
tableName: 'table2',
Parent: function() {
return this.belongsTo(Table1);
}
});
If I'm not select Table1Id
new table1({Id: 1})
.fetchAll({
withRelated: ['Childs':function(qb) {
qb.select('Id', 'Name', 'Description');
}]})
.then(function(rows) {
callback(null, rows);
});
then return empty for Childs[].
Should be;
new table1({Id: 1})
.fetchAll({
withRelated: ['Childs':function(qb) {
qb.select('Id', 'Table1Id', 'Name', 'Description');
}]})
.then(function(rows) {
callback(null, rows);
});
well here's the thing: this can be solved pretty easily, but you NEED to select the primary ID of the table in question, otherwise Bookshelf won't know how to tie the data together. The idea is that you get the query builder from the Knex.js and use the select method (http://knexjs.org/#Builder-select).
Here's the solution for your case:
new table1({
Id: 1
})
.fetchAll({
withRelated: [{
'Childs': function(qb) {
//always select the primary Id of the table, otherwise there will be no relations between the tables
qb.select('Id', 'Name', 'Amount'); //Table1Id is omitted!
}
}]
})
.then(function(rows) {
callback(null, rows);
});
Let me know if this solves your problem.
in your bookshelf.js file, add the visibility plugin as below
bookshelf.plugin('visibility');
in your table2 model, hide the unwanted field(s) as below
table2 = bookshelf.Model.extend({
tableName: 'table2',
hidden: ['Table1Id'],
Parent: function() {
return this.belongsTo(Table1);
}
});
you can learn more about the visibility plugin from here
https://github.com/tgriesser/bookshelf/wiki/Plugin:-Visibility

meteor - How to add a subdocument as reference with SimpleSchema

I have the following SimpleSchema
Schema.Team = new SimpleSchema({
name:{
type:String
},
members: {
type: [Schema.User],
optional:true
}
});
I would like to insert (on the server) a new team document with the current user, as a reference (not as an embedded document).
I have tried:
Teams.insert({name:"theName",members:[Meteor.user()]}) // works but insert the user as an embedded doc.
Teams.insert({name:"theName",members:[Meteor.user()._id]}) // Error: 0 must be an object
I have also tried in two steps:
var id = Teams.insert({name:teamName});
Teams.update({ _id: id },{ $push: { 'users': Meteor.user()._id } });
Then I have another error I don't understand: Error: When the modifier option is true, validation object must have at least one operator
So how can I insert a document with a reference to another schema?
If you just want to store an array of userIds in your Team collection try:
Schema.Team = new SimpleSchema({
name:{
type:String
},
members: {
type: [String],
optional:true
}
});
Then
Teams.insert({ name: "theName", members: [Meteor.userId()] });
Should work. Later when you want to add an additional id you can just:
Teams.update({ _id: teamId },{ $addToSet: { members: Meteor.userId() }});
The following is probably the syntax you are after, assuming you are also using AutoForm.
If you are using collection2, you can also add an autovalue for when a team is created to automatically add the creator to that team for more convenience.
Schema.Team = new SimpleSchema({
name: {
type:String
},
members: {
type: [String],
defaultValue: [],
allowedValues: function () {
// only allow references to the user collection.
return Meteor.users.find().map(function (doc) {
return doc._id
});
},
autoform: {
// if using autoform, this will display their username as the option instead of their id.
options: function () {
return Meteor.users.find().map(function (doc) {
return {
value: doc._id,
label: doc.username // or something
}
})
}
},
autoValue: function () {
if (this.isInsert && !this.isFromTrustedCode) {
return [this.userId];
}
}
}
});

BookshelfJS: How to apply BelongsToMany?

I am confused how to apply BelongsToMany with Bookshelf.
Say, there is a Movie that BelongsToMany Genres, e.g.
"The Artist" has the genres "Comedy, Drama"
I have setup a join table called join_movies_genres that has FK movie_id and genre_id.
I try to fetch genres from a Movie with and without a through(...) definition. I get however undefined targets, similar to:
relatedData:
{ type: 'belongsToMany',
target:
{ [Function]
NotFoundError: [Function: ErrorCtor],
NoRowsUpdatedError: [Function: ErrorCtor],
NoRowsDeletedError: [Function: ErrorCtor] },
targetTableName: 'genres',
targetIdAttribute: 'id',
joinTableName: 'join_movies_genres',
foreignKey: { debug: true },
otherKey: undefined,
parentId: 1,
parentTableName: 'movies',
parentIdAttribute: 'id',
parentFk: 1,
throughTarget:
{ [Function]
NotFoundError: [Function: ErrorCtor],
NoRowsUpdatedError: [Function: ErrorCtor],
NoRowsDeletedError: [Function: ErrorCtor] },
throughTableName: 'join_movies_genres',
throughIdAttribute: 'id',
throughForeignKey: { debug: true } }
So, how would I approach setting up this relation? How can I enable a debug output?
The current state of the Model is:
var Movie = bookshelf.Model.extend({
tableName: 'movies',
genres: function() {
// return this.belongsToMany(Genre, 'join_movies_genres', 'movie_id', 'genre_id', {debug: true});
// return this.belongsToMany(Genre).through(JoinMovieGenre, 'movie_id', 'genre_id');
return this.belongsToMany(Genre, 'join_movies_genres', 'movie_id', 'genre_id').through(JoinMovieGenre, {debug: true});
}
});
var Genre = bookshelf.Model.extend({
tableName: 'genres'
});
new Movie({title: 'The Artist'}).fetch({debug: true}).then(function(m) {
console.log(m.toJSON());
console.log(m.genres())
})
A sandbox of this code is at https://github.com/mulderp/bookshelf-demo/tree/cli_migrations
Does this work?
var Movie = bookshelf.Model.extend({
tableName: 'movies',
genres: function() {
return this.belongsToMany(Genre, 'join_movies_genres', 'movie_id', 'genre_id');
}
});
var Genre = bookshelf.Model.extend({
tableName: 'genres'
});
new Movie({title: 'The Artist'}).fetch({withRelated:['genres']}).then(function(m) {
console.log(m.toJSON());
});

Resources