this.find(...) never reaches callback within static method - javascript

I am working on a module which adds Friendship-based relationships to a Schema.
I'm basically trying to do what this guy is trying to do (which, AFAIK, should work--which is discouraging)
Why is find(...) in FriendshipSchema.statics.getFriends never reaching its callback?
EDIT - Please allow me to explain the expected execution flow...
inside accounts.js:
requires the 'friends-of-friends' module (loads friends-of-friends/index.js) which
requires friends-of-friends/friendship.js which exports a function that creates FriendshipSchema, adds static methods, returns Friendship Model.
requires friends-of-friends/plugin.js which exports the mongoose plugin that adds static and instance methods to `AccountSchema.
uses FriendsOfFriends.plugin (see friends-of-friends/index.js) to plug-in the functionality from friends-of-friends/plugin.js
defines AccountSchema.statics.search which calls this.getFriends.
Since this refers to the Account model once it is compiled, and since the plugin added schema.statics.getFriends, calling this.getFriends within AccountSchema.statics.search will call schema.statics.getFriends as defined in friends-of-friends/plugin.js, which will call Friendship.getFriends (defined by FriendshipSchema.statics.getFriends in friends-of-friends/friendship.js) which calls this.find(...) which should translate to Friendship.find(...)`
after retrieving an account document, I call account.search('foo', function (...) {...});, but as you can see in FriendshipSchema.statics.getFriends, the find method executes, but its callback is never invoked and the program hangs :(
I don't get any errors, so I know this is a logic problem, but I'm not sure why things are getting hung up where they are...
EDIT - see my answer below, I also needed to compile the models before I could call find on them.
account.js
var mongoose = require('mongoose'),
passportLocalMongoose = require('passport-local-mongoose');
var FriendsOfFriends = require('friends-of-friends')();
// define the AccountSchema
// username, password, etc are added by passportLocalMongoose plugin
var AccountSchema = new mongoose.Schema({
created: { type: Date, default: Date.now },
profile: {
displayName: { type: String, required: true, unique : true, index: true },
firstName: { type: String, required: true, trim: true, index: true },
lastName: { type: String, required: true, trim: true, index: true },
}
});
// plugin the FriendsOfFriends plugin to incorporate relationships and privacy
AccountSchema.plugin(FriendsOfFriends.plugin, FriendsOfFriends.options);
AccountSchema.statics.search = function (userId, term, done) {
debug('search')
var results = {
friends: [],
friendsOfFriends: [],
nonFriends: []
},
self=this;
this.getFriends(userId, function (err, friends) {
// never reaches this callback!
});
};
AccountSchema.methods.search = function (term, done) {
debug('method:search')
AccountSchema.statics.search(this._id, term, done);
};
module.exports = mongoose.model('Account', AccountSchema);
friends-of-friends/index.js
/**
* #author Jeff Harris
* #ignore
*/
var debug = require('debug')('friends-of-friends');
friendship = require('./friendship'),
plugin = require('./plugin'),
privacy = require('./privacy'),
relationships = require('./relationships'),
utils = require('techjeffharris-utils');
module.exports = function FriendsOfFriends(options) {
if (!(this instanceof FriendsOfFriends)) {
return new FriendsOfFriends(options);
}
var defaults = {
accountName: 'Account',
friendshipName: 'Friendship',
privacyDefault: privacy.values.NOBODY
};
this.options = utils.extend(defaults, options);
/**
* The Friendship model
* #type {Object}
* #see [friendship]{#link module:friendship}
*/
this.friendship = friendship(this.options);
/**
* mongoose plugin
* #type {Function}
* #see [plugin]{#link module:plugin}
*/
this.plugin = plugin;
debug('this.friendship', this.friendship);
};
friends-of-friends/friendship.js
var debug = require('debug')('friends-of-friends:friendship'),
mongoose = require('mongoose'),
privacy = require('./privacy'),
relationships = require('./relationships'),
utils = require('techjeffharris-utils');
module.exports = function friendshipInit(options) {
var defaults = {
accountName: 'Account',
friendshipName: 'Friendship',
privacyDefault: privacy.values.NOBODY
};
options = utils.extend(defaults, options);
debug('options', options);
var ObjectId = mongoose.Schema.Types.ObjectId;
var FriendshipSchema = new mongoose.Schema({
requester: { type: ObjectId, ref: options.accountName, required: true, index: true },
requested: { type: ObjectId, ref: options.accountName, required: true, index: true },
status: { type: String, default: 'Pending', index: true},
dateSent: { type: Date, default: Date.now, index: true },
dateAccepted: { type: Date, required: false, index: true }
});
...
FriendshipSchema.statics.getFriends = function (accountId, done) {
debug('getFriends')
var model = mongoose.model(options.friendshipName, schema),
friendIds = [];
var conditions = {
'$or': [
{ requester: accountId },
{ requested: accountId }
],
status: 'Accepted'
};
debug('conditions', conditions);
model.find(conditions, function (err, friendships) {
debug('this callback is never reached!');
if (err) {
done(err);
} else {
debug('friendships', friendships);
friendships.forEach(function (friendship) {
debug('friendship', friendship);
if (accountId.equals(friendship.requester)) {
friendIds.push(friendship.requested);
} else {
friendIds.push(friendship.requester);
}
});
debug('friendIds', friendIds);
done(null, friendIds);
}
});
debug('though the find operation is executed...');
};
...
return mongoose.model(options.friendshipName, FriendshipSchema);
};
friends-of-friends/plugin.js
var debug = require('debug')('friends-of-friends:plugin'),
mongoose = require('mongoose'),
privacy = require('./privacy'),
relationships = require('./relationships'),
utils = require('techjeffharris-utils');
module.exports = function friendshipPlugin (schema, options) {
var defaults = {
accountName: 'Account',
friendshipName: 'Friendship',
privacyDefault: privacy.values.NOBODY
};
options = utils.extend(defaults, options);
var Friendship = mongoose.model(options.friendshipName);
...
schema.statics.getFriends = function (accountId, done) {
debug('getFriends')
var model = mongoose.model(options.accountName, schema);
var select = '_id created email privacy profile';
Friendship.getFriends(accountId, function (err, friendIds) {
if (err) {
done(err);
} else {
model.find({ '_id' : { '$in': friendIds } }, select, done);
}
});
};
...
schema.methods.getFriends = function (done) {
schema.statics.getFriends(this._id, done);
};
};

The issue was related to which instance of mongoose was being required.
Within my main app, I was requiring mongoose from app/node_modules/mongoose whereas my friends-of-friends module--having listed mongoose as a dependency in package.json--was requiring mongoose from app/node_modules/friends-of-friends/node_modules/mongoose, which created two separate mongoose instances, which made things not work.
I removed mongoose as a dependency, removed the nested node_modules folder, and vioala, it works, again :)
should have RTFM
app/
| lib/
| node_modules/
| | mongoose/ <-- main app required here
| | friends-of-friends/
| | | node_modules/ <-- deleted; mongoose was only dep
| | | | mongoose/ <-- friends-of-friends module required here
| server.js

Related

Mongoose Schema method: Error - model method is not a function

I have two Mongoose model schemas as follows. The LabReport model contains an array of the referenced SoilLab model. There is a static method in the SoilLab model that I was using to select which fields to display when LabReport is retrieved.
//LabReport.js
var mongoose = require("mongoose");
var SoilLab = mongoose.model("SoilLab");
var LabReportSchema = new mongoose.Schema(
{
labFarm: { type: mongoose.Schema.Types.ObjectId, ref: "Farm" },
testName: { type: String },
soilLabs: [{ type: mongoose.Schema.Types.ObjectId, ref: "SoilLab" }],
},
{ timestamps: true, usePushEach: true }
);
LabReportSchema.methods.toLabToJSON = function () {
return {
labReport_id: this._id,
testName: this.testName,
soilLabs: this.soilLabs.SoilToLabJSON(),
};
};
mongoose.model("LabReport", LabReportSchema);
//SoilLab.js
var mongoose = require("mongoose");
var SoilLabSchema = new mongoose.Schema(
{
description: { type: String },
sampleDate: { type: Date },
source: { type: String },
},
{ timestamps: true, usePushEach: true }
);
SoilLabSchema.methods.SoilToLabJSON = function () {
return {
description: this.description,
sampleDate: this.sampleDate,
source: this.source,
};
};
mongoose.model("SoilLab", SoilLabSchema);
When I try to retrieve the LabReport, I get "this.soilLabs.SoilToLabJSON is not a function". This is how I'm trying to retrieve LabReport.
//labReports.js
...
return Promise.all([
LabReport.find()
.populate("soilLabs")
.exec(),
LabReport.count(query).exec(),
req.payload ? User.findById(req.payload.id) : null,
]).then(function (results) {
var labReports = results[0];
var labReportsCount = results[1];
var user = results[2];
return res.json({
labReports: labReports.map(function (labReport) {
return labReport.toLabToJSON(user); //This cant find SoilToLabJSON
}),
If I remove the .SoilToLabJSON in LabReport.js and just call this.soilLabs, it works but outputs all of the soilLabs data which will become an issue when I have the model completed with more data. I have dug into statics vs methods a little and tried changing it to statics but it didn't work.
I get the soilLabs to populate but not sure why the .SoilToLabJSON method is inaccessible at this point. Do I need to find() or populate the soilLab differently? Is the method incorrect?
labReport.toLabToJSON is passing an array and that was causing the error for me. I simply edited the LabReport.js to the following to take the array and map it to SoilToLabJSON properly.
myTestSoilLabOutput = function (soilLabs) {
var test = soilLabs.map(function (soilLab) {
return soilLab.SoilToLabJSON();
});
return test;
Changed the LabReportSchema.methods.toLabToJSON to:
LabReportSchema.methods.toLabToJSON = function () {
return {
labReport_id: this._id,
testName: this.testName,
soilLabs: myTestSoilLabOutput(this.soilLabs),
};
};

Mongoose.model('model_name') return empty object when function is required in the mongoose model schema file

I apologize if the title of the question is misleading, because I am not too sure how to explain this. I have 2 files, matchedTransaction.js and player.js.
sharedApi/player.js
const MatchedTransactionModel = require('../models/matchedTransaction');
// #1: If I try to console log here, the output will be an empty object "{}"
console.log(MatchedTransactionModel);
async function banPlayer(userId) {
// ...
// Because MatchedTransactionModel is an empty object,
// the code below will crash with the following error:
// "MatchedTransactionModel.findOne is not a function"
const pendingMatchedTransaction = await MatchedTransactionModel.findOne({
$and: [
{
$or: [
{ reserverAccountId: `${account._id}` },
{ sellerAccountId: `${account._id}` },
],
},
{
$or: [
{ status: 'pendingReserverPayment' },
{ status: 'pendingSellerConfirmation' },
],
},
],
});
// ...
}
module.exports = {
banPlayer,
};
models/matchedTransaction.js
const mongoose = require('mongoose');
const { banPlayer } = require('../sharedApi/player');
const MatchedTransactionSchema = new mongoose.Schema([
{
createdDate: {
type: Date,
required: true,
},
// ...
},
]);
MatchedTransactionSchema.post('init', async function postInit() {
// ...
await banPlayer(userId);
});
const MatchedTransactionModel = mongoose.model('matchedTransactions', MatchedTransactionSchema);
module.exports = MatchedTransactionModel;
Notice that in player.js when I tried to console.log the required MatchedTransactionModel, it returns an empty object. However, if I made the following changes to matchedTransaction.js:
models/matchedTransaction.js
// Instead of requiring banPlayer outside, I only require it when it is used
// const { banPlayer } = require('../sharedApi/player');
MatchedTransactionSchema.post('init', async function postInit() {
// ...
const { banPlayer } = require('../sharedApi/player');
await banPlayer(userId);
});
// ...
The output of the previously mentioned console.log will be a non-empty object, and MatchedTransactionModel.findOne is working as expected.
Why does that happen?
The only problem I see with your code is that when you define the schema on matchedTransaction.js, you passed an array which I think is problematic and does not make sense. You must pass an object there:
const MatchedTransactionSchema = new mongoose.Schema({
createdDate: {
type: Date,
required: true,
},
// ...
});

Custom validator, cb is not a function

Problem with a custom validator in node.js, using mongoose. I'm trying to check if a query exists in headerLog prior to inserting it.
My code is below:
var mongoose = require('mongoose'); //layer above mongodb
var Schema = mongoose.Schema;
var headerLogSchema = new Schema({
query: { type: String, required: true, unique: true, validate: {
validator: function(v, cb) {
HeaderLog.find({query: v}, function(err, documents){
cb(documents.length == 0);
});
},
message: 'Header already exists in log, didnt save this one.'
}
}
})
var HeaderLog = mongoose.model('headerLog', headerLogSchema);
module.exports = HeaderLog;
The error: TypeError: cb is not a function.
I'm calling this function like so:
function logHeader(query) {
var newHeaderLog = new HeaderLog({
query: query
})
newHeaderLog.save(function(err) {
if (err) {
console.log(err);
}
else {
console.log('New header logged');
}
});
}
What am I doing wrong?
As the reference states, asynchronous validators should either have isAsync flag:
validate: {
isAsync: true,
validator: function(v, cb) { ... }
}
Or return a promise. Since the validator already uses another model, and Mongoose models are promise-based, it makes sense to use existing promise:
validator: function(v) {
return HeaderLog.find({query: v}).then(documents => !documents.length);
}
countDocuments is a better alternative to find for cases when only documents count is needed.
If you look at the async validator example here in the doc, it looks like you have to pass the option isAsync: true in order to tell mongoose that you are using an async validator and thus it should pass a callback to it.
var headerLogSchema = new Schema({
query: {
type: String,
required: true,
unique: true,
validate: {
isAsync: true, // <======= add this
validator: function(v, cb) {
HeaderLog.find({query: v}, function(err, documents){
cb(documents.length == 0);
});
},
message: 'Header already exists in log, didnt save this one.'
}
}
})

How to make a custom route for users? And how to add hooks to it?

I'm trying to add a route /me to get user authenticated information. This is what I have at my files.
I've tried adding a route /me at users.services file, but I'm getting this error: "error: MethodNotAllowed: Method find is not supported by this endpoint."
I want to get response with a user object (based on token) to a GET method to route '/me'.
users.service.js
// Initializes the `users` service on path `/users`
const createService = require('feathers-sequelize');
const createModel = require('../../models/users.model');
const hooks = require('./users.hooks');
module.exports = function (app) {
const Model = createModel(app);
const paginate = app.get('paginate');
const options = {
name: 'users',
Model,
paginate
};
// Initialize our service with any options it requires
app.use('/users', createService(options));
app.use('/me', {
get(id, params) {
return Promise.resolve([
{
id: 1,
text: 'Message 1'
}
])
}
})
// Get our initialized service so that we can register hooks and filters
const service = app.service('users');
service.hooks(hooks);
};
users.hooks.js
const { authenticate } = require('#feathersjs/authentication').hooks;
const {
hashPassword, protect
} = require('#feathersjs/authentication-local').hooks;
module.exports = {
before: {
all: [ ],
find: [ authenticate('jwt') ],
get: [],
create: [ hashPassword() ],
update: [ hashPassword() ],
patch: [ hashPassword() ],
remove: []
},
after: {
all: [
// Make sure the password field is never sent to the client
// Always must be the last hook
protect('password')
],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};
users.model.js
// See http://docs.sequelizejs.com/en/latest/docs/models-definition/
// for more of what you can do here.
const Sequelize = require('sequelize');
const DataTypes = Sequelize.DataTypes;
module.exports = function (app) {
const sequelizeClient = app.get('sequelizeClient');
const users = sequelizeClient.define('users', {
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
password: {
type: DataTypes.STRING,
allowNull: false
},
}, {
hooks: {
beforeCount(options) {
options.raw = true;
}
}
});
users.associate = function (models) { // eslint-disable-line no-unused-vars
// Define associations here
// See http://docs.sequelizejs.com/en/latest/docs/associations/
};
return users;
};
What you did through
app.use('/me', {
get(id, params) {
return Promise.resolve([
{
id: 1,
text: 'Message 1'
}
])
}
})
Was implement routes for /me/:id. The find method is what runs for the base route of /me.
I don't think a separate service is really necessary though. An easier solution would be to use a before all hook that changes the id if you are accessing /users/me:
module.exports = function() {
return async context => {
if(context.id === 'me') {
context.id = context.params.user._id;
}
}
}

Sequelize associations: set[Models] adds new models instead of associating existing ones

I'm using Sequelize and I'm trying to create associations between two different tables, where x.belongsTo(y) and y.hasMany(x). After having done x.setY(yInstance) and y.getXs() it seems only new rows have been added to x and no associations to my already created instances have been created.
var Promise = require("bluebird"),
Sequelize = require("sequelize");
var sequelize = new Sequelize("Test", "postgres", "password", {
host: "localhost",
dialect: "postgres",
pool: {
max: 5,
min: 0,
idle: 10000
}
});
var Schedule = sequelize.define("Schedule", {
website: {
type: Sequelize.STRING
}
});
var SiteConfig = sequelize.define("SiteConfig", {
systemType: {
type: Sequelize.STRING
}
});
var Selector = sequelize.define("Selector", {
type: {
type: Sequelize.STRING
},
content: {
type: Sequelize.STRING
}
});
Selector.belongsTo(SiteConfig);
SiteConfig.hasMany(Selector);
var testSchedule = {
website: "google.com"
};
var testSiteConfig = {
systemType: "one"
};
var testSelectors = [
{type: "foo", content: "foo"},
{type: "foo", content: "bar"}
];
Promise.all([
Schedule.sync({force: true}),
SiteConfig.sync({force: true}),
Selector.sync({force: true})
]).then(function () {
return Promise.all([
Schedule.create(testSchedule),
SiteConfig.create(testSiteConfig),
Selector.bulkCreate(testSelectors)
]);
}).spread(function (schedule, siteConfig, selectors) {
return Promise.map(selectors, function (selector) {
return selector.setSiteConfig(siteConfig);
}).then(function (array) {
return siteConfig.getSelectors();
}).each(function (selector) {
// This is where I expect "foo" and "bar" but instead get null
console.log("Selector content:", selector.get("content"));
});
});
I'd expect this code to add a SiteConfigId column to my Selectors so that my siteConfig.getSelectors() would return my testSelectors. How can I achieve this?
[UPDATE]
It turns out what I had earlier was wrong. The method setSiteConfig() is not what you want to use. I checked the db and it looks like Sequelize created two new records instead of associating the existing foo/bar selectors:
test=# select * from "Selectors";
id | type | content | createdAt | updatedAt | SiteConfigId
----+------+---------+----------------------------+----------------------------+--------------
1 | foo | foo | 2015-04-05 20:38:55.282-07 | 2015-04-05 20:38:55.282-07 |
2 | foo | bar | 2015-04-05 20:38:55.282-07 | 2015-04-05 20:38:55.282-07 |
3 | | | 2015-04-05 20:38:55.282-07 | 2015-04-05 20:38:55.311-07 | 1
4 | | | 2015-04-05 20:38:55.282-07 | 2015-04-05 20:38:55.31-07 | 1
So what is different? You can't use setSiteConfig on the child rows, instead you call addSelectors on siteConfig and pass in the selectors you want to associate. See updated code below.
Changed Promise variable to BPromise because node has a native Promise module now which would cause a conflict. Also I believe Sequelize has bluebird built-in so you can also just use Sequelize.Promise.
Removed the nested promise in your spread call because there is no need for it.
Side note: Promise.all returns a single result array so I don't think you should be using .spread().
var BPromise = require("bluebird");
var Sequelize = require("sequelize");
var sequelize = new Sequelize('test', 'root', 'password', {
host: "localhost",
dialect: "postgres",
pool: {
max: 5,
min: 0,
idle: 10000
}
});
var Schedule = sequelize.define("Schedule", {
website: {
type: Sequelize.STRING
}
});
var SiteConfig = sequelize.define("SiteConfig", {
systemType: {
type: Sequelize.STRING
}
});
var Selector = sequelize.define("Selector", {
type: {
type: Sequelize.STRING
},
content: {
type: Sequelize.STRING
}
});
Selector.belongsTo(SiteConfig);
SiteConfig.hasMany(Selector);
var testSchedule = {
website: "google.com"
};
var testSiteConfig = {
systemType: "one"
};
var testSelectors = [
{type: "foo", content: "foo"},
{type: "foo", content: "bar"}
];
sequelize.sync({ force: true })
.then(function(result) {
return BPromise.all([
Schedule.create(testSchedule),
SiteConfig.create(testSiteConfig),
Selector.bulkCreate(testSelectors, { returning: true })
]);
})
.then(function(result) {
var siteConfig = result[1];
var selectors = result[2];
return siteConfig.addSelectors(selectors);
})
.then(function (result) {
return this.siteConfig.getSelectors();
})
.each(function(result) {
console.log('boomshakalaka:', result.get());
})
.catch(function(error) {
console.log(error);
});

Categories

Resources