I'm trying to update the JSON field "champ_x" from 1 to 3 and for both players 1 at a time in a dynamic function:
{
"_id": {
"$oid": "58a3521edf127d0a0c417cda"
},
"room": "room_0.0940045412694186",
"player_1": "eee",
"player_2": "fff",
"player_1_details": {
"history_moves": [],
"champions": [
{
"champ_1": "na"
},
{
"champ_2": "na"
},
{
"champ_3": "na"
}
]
},
"player_2_details": {
"history_moves": [],
"champions": [
{
"champ_1": "na"
},
{
"champ_2": "na"
},
{
"champ_3": "na"
}
]
},
"game_state": "789",
"__v": 0
}
I've got this model:
match_schema.statics.update_champ = function(room, turn, champ_num, champ_select, callback){
if(champ_num == "champ_1"){
match_mongoose.update({ room: room }, { $set: { 'player_1_details.champions.0.champ_1': champ_select}})
.exec(function(error){
if(error){ return callback(error); }else{ return callback(null); }
});
}
};
This model works fine
My problem is, I'm trying to make it dynamic, in which I can just send through the function parameters the current turn(1 or 2), and the chosen position(champ_1,2, or 3).
I've tried this:
//Update Champion
match_schema.statics.update_champ = function(room, turn, champ_num, champ_select, callback){
match_mongoose.update({ room: room }, { $set: { 'player_'+turn+'_details.champions.0.'+champ_num: champ_select}})
.exec(function(error){
if(error){ return callback(error); }else{ return callback(null); }
});
};
var match_mongoose = mongoose.model('matches', match_schema, 'matches');
module.exports = match_mongoose;
But I get an error that says the "Unexpected token +" seems like concatenating the value doesn't work. Is there a way to do this?
Thanks!
You may build the $set modifier and the match part as suggested by #dNitro :
var modifier = { $set: {} };
modifier.$set['player_' + turn + '_details.champions.$.champ_' + champ_num] = champ_select;
You will have also an issue with array index, you specify champions.0 so it will always take the first array item which won't match for champs_2 & champs_3. One solution for this is to use positional parameter $ with a match from the array :
var match = {};
match['room'] = room;
match['player_' + turn + '_details.champions.champ_' + champ_num] = { $exists: true };
The full update function is :
matchSchema.statics.update_champ = function(room, turn, champ_num, champ_select, callback) {
var modifier = { $set: {} };
modifier.$set['player_' + turn + '_details.champions.$.champ_' + champ_num] = champ_select;
var match = {};
match['room'] = room;
match['player_' + turn + '_details.champions.champ_' + champ_num] = { $exists: true };
this.update(match, modifier)
.exec(function(error) {
if (error) {
return callback(error);
} else {
return callback(null);
}
});
};
And calling it with :
Match.update_champ("room_0.0940045412694186", 1, 1, "new_value", function(err, res) {
if (!err) {
console.log(err);
return;
}
console.log(res);
});
You can find a full example here
Related
I wish to update a property per object in array of objects, but if some of the objects doesn't exists, insert the object instead.
Currently I used "upsert", which creates a new document when no document matches the query, unfortunately it is replacing a single item with my entire list.
Worth to mention that I am using mongoist to perform an async requests.
My code:
this.tokenArray = [
{ token: "654364543" },
{ token: "765478656" },
{ token: "876584432" },
{ token: "125452346" },
{ token: "874698557" },
{ token: "654364543" }
]
database.upsertDatebaseItem(this.tokenArray.map(x => { return x.token }), { valid : true }, 'Tokens');
async upsertDatebaseItem(itemKey, itemValue, collectionName) {
try {
await this.database[collectionName].update({ token : { $in: itemKey}}, { $set: itemValue }, {upsert : true} , {multi : true});
} catch (error) {
console.log(`An error occurred while attempting to update ${itemType} to the database: ${error}`);
return false;
}
}
Found the way to do it:
const bulkUpdate = this.tokenArray.map((x) => {
return {
"updateOne": {
"filter": { "token": x.token },
"update": { "$set": { "valid": true } },
"upsert": true
}
};
});
and:
this.database[collectionName].bulkWrite(bulkUpdate);
To upsert with mongoist, use the following:
var bulk = db.collection.initializeOrderedBulkOp()
for(var doc of docs) bulk.find( { _id: doc._id } ).upsert().updateOne(doc); // or use replaceOne()
await bulk.execute();
Converted to your case that would be
var bulk = db.collectionName.initializeOrderedBulkOp()
for(var tokenItem of tokenArray) bulk.find( { token : tokenItem.token } ).upsert().updateOne(tokenItem); // or use replaceOne()
await bulk.execute();
I would like to perform a search request like https://api.mywebsite.com/users/search?firstname=jo&lastname=smit&date_of_birth=1980
I have a User schema like:
var UserSchema = new mongoose.Schema({
role: { type: String, default: 'user' },
firstname: { type: String, default: null },
lastname: { type: String, default: null },
date_of_birth: { type: Date, default: null, select: false },
});
What I did so far with stackoverflow help:
// check every element in the query and perform check function
function search_t(query) {
return function (element) {
for (var i in query) {
if (query[i].function(element[i], query[i].value) == false) {
return false;
}
}
return true;
}
}
// prepare query object, convert elements and add check function
// convert functions are used to convert string (from the query) in the right format
// check functions are used to check values from our users
function prepareSearch(query, cb) {
let fields = {
"firstname": {
"type": "string",
"function": checkString,
"convert": convertString
},
"lastname": {
"type": "string",
"function": checkString,
"convert": convertString
},
"date_of_birth": {
"type": "date",
"function": checkDate,
"convert": convertDate
}
};
for (let k in query) {
k = k.toLowerCase();
if (!(k in fields)) {
return cb({message: "error"});
}
query[k] = {value: fields[k].convert(query[k]), function: fields[k].function};
}
return cb(null, query);
}
// linked to a route like router.get('/search/', controller.search);
export function search(req, res) {
return User.find({}).exec()
.then(users => {
return prepareSearch(req.query, (err, query) => {
if (err) {
return handleError(res)(err);
} else {
return res.status(200).send(users.filter(search_t(query)));
}
});
})
.catch(handleError(res));
}
So this code works but I don't know if it's a good thing. I have a lot of other fields to check (like gender, ....) and I don't know if it's a good thing to do it "manually".
I don't know if mongoose has any function to do it.
Should I use another method to filter / search in my users in my REST API ?
I'm pretty new here and I am not sure about how I work...
Thank you,
Ankirama
In the below code from MongoDB's course Week 3's Query Operators in the Node.js Driver chapter :
var MongoClient = require('mongodb').MongoClient,
commandLineArgs = require('command-line-args'),
assert = require('assert');
var options = commandLineOptions();
MongoClient.connect('mongodb://localhost:27017/crunchbase', function(err, db) {
assert.equal(err, null);
console.log("Successfully connected to MongoDB.");
var query = queryDocument(options);
var projection = {
"_id": 1,
"name": 1,
"founded_year": 1,
"number_of_employees": 1,
"crunchbase_url": 1
};
var cursor = db.collection('companies').find(query, projection);
var numMatches = 0;
cursor.forEach(
function(doc) {
numMatches = numMatches + 1;
console.log(doc);
},
function(err) {
assert.equal(err, null);
console.log("Our query was:" + JSON.stringify(query));
console.log("Matching documents: " + numMatches);
return db.close();
}
);
});
function queryDocument(options) {
console.log(options);
var query = {
"founded_year": {
"$gte": options.firstYear,
"$lte": options.lastYear
}
};
if ("employees" in options) {
query.number_of_employees = {
"$gte": options.employees
};
}
return query;
}
function commandLineOptions() {
var cli = commandLineArgs([{
name: "firstYear",
alias: "f",
type: Number
}, {
name: "lastYear",
alias: "l",
type: Number
}, {
name: "employees",
alias: "e",
type: Number
}]);
var options = cli.parse()
if (!(("firstYear" in options) && ("lastYear" in options))) {
console.log(cli.getUsage({
title: "Usage",
description: "The first two options below are required. The rest are optional."
}));
process.exit();
}
return options;
}
I'm requiring command-line-args package, which has a method commandLineArgs. All good and fine...
Now, I see that the type of the objects passed to this method is set to Number. We can clearly see that they're Strings.
How is it possible?
From the command-line-args GitHub page:
The type value is a setter function (you receive the output from this), enabling you to be specific about the type and value received.
In other words, passing Number as type allows you to parse the arguments as numbers.
I'm trying to use ES6 Classes to construct data models (from a MySQL database) in an API that I'm building. I prefer not using an ORM/ODM library, as this will be a very basic, simple API. But, I'm struggling to get my head around how to define these models.
My data entities are (these are just some simplified examples):
CUSTOMER
Data Model
id
name
groupId
status (enum of: active, suspended, closed)
Private Methods
_getState(status) {
var state = (status == 'active' ? 'good' : 'bad');
return state;
}
Requests
I want to be able to do:
findById: Providing a single customer.id, return the data for that specific customer, i.e. SELECT * FROM customers WHERE id = ?
findByGroupId: Providing a group.id, return the data for all the customers (in an array of objects), belonging to that group, i.e. SELECT * FROM customers WHERE groupId = ?
Response Payloads
For each customer object, I want to return JSON like this:
findById(1);:
[{
"id" : 1,
"name" : "John Doe",
"groupId" : 2,
"status" : "active",
"state" : "good"
}]
findByGroupId(2);:
[{
"id" : 1,
"name" : "John Doe",
"groupId" : 2,
"status" : "active",
"state" : "good"
},
{
"id" : 4,
"name" : "Pete Smith",
"groupId" : 2,
"status" : "suspended",
"state" : "bad"
}]
GROUP
Data Model
id
title
Requests
I want to be able to do:
findById: Providing a single group.id, return the data for that specific group, i.e. SELECT * FROM groups WHERE id = ?
Response Payloads
For each group object, I want to return JSON like this:
findById(2);:
{
"id" : 2,
"title" : "This is Group 2",
"customers" : [{
"id" : 1,
"name" : "John Doe",
"groupId" : 2,
"status" : "active",
"state" : "good"
},
{
"id" : 4,
"name" : "Pete Smith",
"groupId" : 2,
"status" : "suspended",
"state" : "bad"
}]
}
Requirements:
Must use ES6 Classes
Each model in its own file (e.g. customer.js) to be exported
Questions:
My main questions are:
Where would I define the data structure, including fields that require data transformation, using the private methods (e.g. _getState())
Should the findById, findByGroupId, etc by defined within the scope of the class? Or, should these by separate methods (in the same file as the class), that would instantiate the object?
How should I deal with the case where one object is a child of the other, e.g. returning the Customer objects that belongs to a Group object as an array of objects in the Group's findById?
Where should the SQL queries that will connect to the DB be defined? In the getById, getByGroupId, etc?
UPDATE!!
This is what I came up with - (would be awesome if someone could review, and comment):
CUSTOMER Model
'use strict';
class Cust {
constructor (custData) {
this.id = custData.id;
this.name = custData.name;
this.groupId = custData.groupId;
this.status = custData.status;
this.state = this._getState(custData.status);
}
_getState(status) {
let state = (status == 'active' ? 'good' : 'bad');
return state;
}
}
exports.findById = ((id) => {
return new Promise ((resolve, reject) => {
let custData = `do the MySQL query here`;
let cust = new Cust (custData);
let Group = require(appDir + process.env.PATH_API + process.env.PATH_MODELS + 'group');
Group.findById(cust.groupId).then(
(group) => {
cust.group = group;
resolve (cust)
},
(err) => {
resolve (cust);
}
);
});
});
GROUP Model
'use strict';
class Group {
constructor (groupData) {
this.id = groupData.id;
this.title = groupData.title;
}
}
exports.findById = ((id) => {
return new Promise ((resolve, reject) => {
let groupData = `do the MySQL query here`;
if (id != 2){
reject('group - no go');
};
let group = new Group (groupData);
resolve (group);
});
});
CUSTOMER Controller (where the Customer model is instantiated)
'use strict';
var Cust = require(appDir + process.env.PATH_API + process.env.PATH_MODELS + 'cust');
class CustController {
constructor () {
}
getCust (req, res) {
Cust.findById(req.params.id).then(
(cust) => {
res(cust);
},
(err) => {
res(err);
}
)
}
}
module.exports = CustController;
This seems to be working well, and I've been able to use Class, Promise and let to make it more ES6 friendly.
So, I'd like to get some input on my approach. Also, am I using the export and required features correctly in this context?
Here is another approach,
Where would I define the data structure, including fields that require data transformation, using the private methods (e.g. _getState())
You should define those fields, relationship in your model class extending the top model. Example:
class Group extends Model {
attributes() {
return {
id: {
type: 'integer',
primary: true
},
title: {
type: 'string'
}
};
}
relationships() {
return {
'Customer': {
type: 'hasMany',
foreignKey: 'groupId'
}
};
}
}
Should the findById, findByGroupId, etc by defined within the scope of the class? Or, should these by separate methods (in the same file as the class), that would instantiate the object?
Instead of having many functions use findByAttribute(attr) in Model Example:
static findByAttribute(attr) {
return new Promise((resolve, reject) => {
var query = this._convertObjectToQueriesArray(attr);
query = query.join(" and ");
let records = `SELECT * from ${this.getResourceName()} where ${query}`;
var result = this.run(records);
// Note: Only support 'equals' and 'and' operator
if (!result) {
reject('Could not found records');
} else {
var data = [];
result.forEach(function(record) {
data.push(new this(record));
});
resolve(data);
}
});
}
/**
* Convert Object of key value to sql filters
*
* #param {Object} Ex: {id:1, name: "John"}
* #return {Array of String} ['id=1', 'name=John']
*/
static _convertObjectToQueriesArray(attrs) {
var queryArray = [];
for (var key in attrs) {
queryArray.push(key + " = " + attrs[key]);
}
return queryArray;
}
/**
* Returns table name or resource name.
*
* #return {String}
*/
static getResourceName() {
if (this.resourceName) return this.resourceName();
if (this.constructor.name == "Model") {
throw new Error("Model is not initialized");
}
return this.constructor.name.toLowerCase();
}
How should I deal with the case where one object is a child of the other, e.g. returning the Customer objects that belongs to a Group object as an array of objects in the Group's findById?
In case of relationships, you should have methods like findRelations, getRelatedRecords.
var customer1 = new Customer({ id: 1, groupId: 3});
customer1.getRelatedRecords('Group');
class Model {
...
getRelatedRecords(reln) {
var targetRelationship = this.relationships()[reln];
if (!targetRelationship) {
throw new Error("No relationship found.");
}
var primaryKey = this._getPrimaryKey();
var relatedObject = eval(reln);
var attr = {};
if (targetRelationship.type == "hasOne") {
console.log(this.values);
attr[relatedObject.prototype._getPrimaryKey()] = this.values[targetRelationship.foreignKey];
} else if (targetRelationship.type == "hasMany") {
attr[targetRelationship.foreignKey] = this.values[this._getPrimaryKey()];
}
relatedObject.findByAttribute(attr).then(function(records) {
// this.values[reln] = records;
});
}
...
}
Where should the SQL queries that will connect to the DB be defined? In the getById, getByGroupId, etc?
This one is tricky, but since you want your solution to be simple put the queries inside your find methods. Ideal scenario will be to have their own QueryBuilder Class.
Check the following full code the solution is not fully functional but you get the idea. I've also added engine variable in the model which you can use to enhance fetching mechanism. All other design ideas are upto your imagination :)
FULL CODE:
var config = {
engine: 'db' // Ex: rest, db
};
class Model {
constructor(values) {
this.values = values;
this.engine = config.engine;
}
toObj() {
var data = {};
for (var key in this.values) {
if (this.values[key] instanceof Model) {
data[key] = this.values[key].toObj();
} else if (this.values[key] instanceof Array) {
data[key] = this.values[key].map(x => x.toObj());
} else {
data[key] = this.values[key];
}
}
return data;
}
static findByAttribute(attr) {
return new Promise((resolve, reject) => {
var query = this._convertObjectToQueriesArray(attr);
query = query.join(" and ");
let records = `SELECT * from ${this.getResourceName()} where ${query}`;
var result = this.run(records);
// Note: Only support 'equals' and 'and' operator
if (!result) {
reject('Could not found records');
} else {
var data = [];
result.forEach(function(record) {
data.push(new this(record));
});
resolve(data);
}
});
}
getRelatedRecords(reln) {
var targetRelationship = this.relationships()[reln];
if (!targetRelationship) {
throw new Error("No relationship found.");
}
var primaryKey = this._getPrimaryKey();
var relatedObject = eval(reln);
var attr = {};
if (targetRelationship.type == "hasOne") {
console.log(this.values);
attr[relatedObject.prototype._getPrimaryKey()] = this.values[targetRelationship.foreignKey];
} else if (targetRelationship.type == "hasMany") {
attr[targetRelationship.foreignKey] = this.values[this._getPrimaryKey()];
}
relatedObject.findByAttribute(attr).then(function(records) {
// this.values[reln] = records;
});
}
/**
* Test function to show what queries are being ran.
*/
static run(query) {
console.log(query);
return [];
}
_getPrimaryKey() {
for (var key in this.attributes()) {
if (this.attributes()[key].primary) {
return key;
}
}
}
/**
* Convert Object of key value to sql filters
*
* #param {Object} Ex: {id:1, name: "John"}
* #return {Array of String} ['id=1', 'name=John']
*/
static _convertObjectToQueriesArray(attrs) {
var queryArray = [];
for (var key in attrs) {
queryArray.push(key + " = " + attrs[key]);
}
return queryArray;
}
/**
* Returns table name or resource name.
*
* #return {String}
*/
static getResourceName() {
if (this.resourceName) return this.resourceName();
if (this.constructor.name == "Model") {
throw new Error("Model is not initialized");
}
return this.constructor.name.toLowerCase();
}
}
class Customer extends Model {
attributes() {
return {
id: {
type: 'integer',
primary: true
},
name: {
type: 'string'
},
groupId: {
type: 'integer'
},
status: {
type: 'string'
},
state: {
type: 'string'
}
};
}
relationships() {
return {
'Group': {
type: 'hasOne',
foreignKey: 'groupId'
}
};
}
}
class Group extends Model {
attributes() {
return {
id: {
type: 'integer',
primary: true
},
title: {
type: 'string'
}
};
}
relationships() {
return {
'Customer': {
type: 'hasMany',
foreignKey: 'groupId'
}
};
}
}
var cust = new Customer({
id: 1,
groupId: 3
});
cust.getRelatedRecords('Group');
var group = new Group({
id: 3,
title: "Awesome Group"
});
group.getRelatedRecords('Customer');
var groupData = new Group({
"id": 2,
"title": "This is Group 2",
"customers": [new Customer({
"id": 1,
"name": "John Doe",
"groupId": 2,
"status": "active",
"state": "good"
}),
new Customer({
"id": 4,
"name": "Pete Smith",
"groupId": 2,
"status": "suspended",
"state": "bad"
})
]
});
console.log(groupData.toObj());
I have the following JS object:
{
"gameName":"Shooter",
"details":[
{
"submitted":1415215991387,
"author":"XYZ",
"subPlayer":{
"members":{
"squad1":[
{
"username":"John",
"deaths":0
}
]
},
"gameSlug":"0-shooter"
}
}
],
"userId":"foL9NpoZFq9AYmXyj",
"author":"Peter",
"submitted":1415215991608,
"lastModified":1415215991608,
"participants":[
"CXRR4sGf5AdvSjdgc",
"foL9NpoZFq9AYmXyj"
],
"slug":"1-shooterConv",
"_id":"p2QQ4TBwidjeZX6YS"
}
And I want to get the deaths for the correct user.
My current code is something like this:
$.map(this.details.subPlayer.members.squad1, function(obj) {
if(obj.username == Meteor.user().username) {
return obj.deaths;
}
});
However, the problem is that I don't know exactly the squad name (e.g. squad1). How can I get deaths for the corresponding username in any squad?
Any help would be greatly appreciated.
Using underscore, You can use: data.details[0].subPlayer.members
var data = {
"gameName":"Shooter",
"details":[
{
"submitted":1415215991387,
"author":"XYZ",
"subPlayer":{
"members":{
"squad1":[
{
"username":"John",
"deaths":0
}
]
},
"gameSlug":"0-shooter"
}
}
],
"userId":"foL9NpoZFq9AYmXyj",
"author":"Peter",
"submitted":1415215991608,
"lastModified":1415215991608,
"participants":[
"CXRR4sGf5AdvSjdgc",
"foL9NpoZFq9AYmXyj"
],
"slug":"1-shooterConv",
"_id":"p2QQ4TBwidjeZX6YS"
}
function getDeaths(data) {
var members = data.details[0].subPlayer.members;
return _.map(members, function(key, value){
return {
member: value,
deaths: members[value][0].deaths
}
});
}
console.log(JSON.stringify(getDeaths(data), null, ' '));
This returns:
[
{
"member": "squad1",
"deaths": 0
}
]
You can filter the results by squad and username. The squadname is optional, but the username must match.
function getDeaths(data, username, squad) {
var users = [];
var members = data.details[0].subPlayer.members;
_.each(members, function(squads, squadName) {
if (!squad || (squad && squadName === squad)) {
_.each(squads, function(user, index) {
if (user.username === username) {
users.push(user);
}
})
}
});
return users;
}
var deaths = getDeaths(data, 'John', 'squad1');
console.log(JSON.stringify(deaths, null, ' '));