Mongoose custom SchemaType toJSON/Transform - node.js

For an application I'm currently working on I need to store dates without a time. I've done this by creating a custom schema type that looks something like so:
var mongoose = require('mongoose');
/**
* Registers a new DateOnly type field. Extends the `Date` Schema Type
*/
function DateOnly(key, options) {
mongoose.SchemaTypes.Date.call(this, key, options, 'DateOnly');
}
DateOnly.prototype = Object.create(mongoose.SchemaTypes.Date.prototype);
DateOnly.prototype.cast = (originalValue) => {
try {
var value = originalValue;
if (typeof value === 'string' && !value.match(/^([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))(T00:00:00.000Z)?$/)) {
throw new Error('Date is invalid');
} else if (typeof value === 'number') {
value = new Date(value);
}
if (value instanceof Date) {
value = new Date(value.getFullYear(), value.getMonth(), value.getDate());
}
return mongoose.Schema.Types.Date._cast(value);
} catch (err) {
throw new mongoose.SchemaType.CastError('date', originalValue, this.path);
}
};
mongoose.Schema.Types.DateOnly = DateOnly;
module.exports = DateOnly;
Which allows the model to accept date strings (ex: 2020-01-01) and date objects. Now this will store all dates at midnight UTC time, that way I still get all the advantages of them being stored as dates in mongodb.
Where my issue comes in is that when I'm returning one of these dates to the API it gets returned in full ISO format (ex: 2020-01-01T00:00:00.000Z), which will get converted into the local user's time zone. In my timezone, this date will show up as 1 day earlier than desired.
So my question is, how can I make it so that when document.toJSON is called the date is transformed? I know that what I want to be returning is date.toISOString().substring(0,10).
I've tried inheriting from the Date class, but I discovered it isn't compatible with how mongoose and the mongodb driver work.
I know I could write a method to put in the toJSON.transform options, but then I'd have to do this for every field and model that uses the type.

A solution for this was added in mongoose 5.9.0, and can be done like so:
DateOnly.set('transform', (val) => {
return /* transformed value */;
});
Source: https://github.com/Automattic/mongoose/issues/8403

Related

How to query by date on cloud function?

I'm trying to query some data that is between two dates, but nothing seems to work. These are my dates, I saw on a post that the division by 1000 should work , but it's not. Ive tried firebase.firestore but says firebase it's not defined but I don't know how to simply reference to firebase.firestore.Timestamp
let now = new Date()
let yesterday = Math.round((new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1).getTime())/1000)
now = Math.round(now.getTime()/1000)
This is my query attempt. Nothing works and it just returns my empty array because the query has no data to iterate through the forEach.
let snapshot = await db.collection('appointments')
.where('status', '==', 'Pending')
.where('startDate', '<=', now)
.where('startDate', '>', yesterday)
.get().then(docs => {
docs.forEach(snapshot => {
console.log(snapshot.id, '=>', snapshot.data());
console.log(snapshot.data()['doctor']);
doctor_mail.push(snapshot.id)
doctor_mail.push(snapshot.data())
});
return doctor_mail
}).
catch(err => {
return res.send(err)
});
console.log(doctor_mail)
res.send(snapshot)
The query is not returning anything because there is nothing to return. If you are querying for a string representing a date and the data is a timestamp in the Firestore, so in order to know that, it would be need to check a sample document to compare, since in the first part of your question you mentioned that you want to get a Firestore timestamp, you can do it with this code:
const timestamp = db.FieldValue.serverTimestamp();
//if you want it as a date object
const date = timestamp.toDate();
As per what you asked in the comments, for getting the value of Today and Yesterday in Timestamp you can do the following:
var todayTimestamp = timestamp.now();
var date = new Date().setDate(date.getDate() - 1);
var yesterdayTimestamp = timestamp.fromDate(date);
And convert them back to date so you can operate them if needed, you can check more details on the Timestamp in this Documentation
Okay, the thing here was that I was wrongly calling db as const db = firebase.firestore.
Instead I just had to go for:
const db = admin.firestore()

Mongoose, NodeJS & Express: Sorting by column given by api call

I'm currently writing a small API for a cooking app. I have a Recipe model and would like to implement sorting by columns based on the req Parameter given.
I'd like to sort by whatever is passed in the api call. the select parameter works perfectly fine, I can select the columns to be displayed but when I try to sort anything (let's say by rating) the return does sort but I'm not sure what it does sort by.
The code i'm using:
query = Recipe.find(JSON.parse(queryStr));
if(req.query.select){
const fields = req.query.select.split(',').join(' ');
query = query.select(fields);
}
if(req.query.sort){
const sortBy = req.query.sort.split(',').join(' ');
query = query.sort({ sortBy: 1 });
} else {
query = query.sort({ _id: -1 });
}
The result, when no sorting is set: https://pastebin.com/rPLv8n5s
vs. the result when I pass &sort=rating: https://pastebin.com/7eYwAvQf
also, when sorting my name the result is also mixed up.
You are not using the value of sortBy but the string "sortBy". You will need to create an object that has the rating as an object key.
You need the sorting object to look like this.
{
rating: 1
}
You can use something like this so it will be dynamic.
if(req.query.sort){
const sortByKey = req.query.sort.split(',').join(' ');
const sortByObj = {};
sortByObj[sortByKey] = 1; // <-- using sortBy as the key
query = query.sort(sortByObj);
} else {
query = query.sort({ _id: -1 });
}

Get missing fields on mongoose scheme

I need to get a count of all the missing or empty fields in mongoose schema
The idea is to have a list of all the properties in a mongoose schema that are blank or does not have a value, so I can make a function to know what percentage of the document is missing.
I tried with count and count by null, but i don't know the query to get the results.
You can have a function like this in your application code:
const getDocumentCompleteness = (doc) => {
const totaFieldCount = Object.keys(doc).length;
let completedFieldCount = 0;
for(let key in doc) {
if(doc[key] !== undefined && doc[key] !== null && doc[key] !== '') {
completedFieldCount += 1;
}
}
return [totaFieldCount, completedFieldCount];
}
Whenever you need to get the completeness of a document, you can do this:
const [totaFieldCount, completedFieldCount] = getDocumentCompleteness(document);
// where document is the object representing the document you got from the database
I hope that helps.

Mongoose update dependent field from another field's setter?

I have a scenario in node/express/mongoose where I have users who accumulate points, and when that point total crosses a certain threshold, they "level up" (think games with point-based levels).
I have created a custom setter on the points field that checks if the value has changed, and if so tries to update the level field. Levels are defined in another collection, but are saved as simple JSON objects when associated with user docs (hence the .lean() in the query). I did it this way vs a virtual field or population for efficiency.
Problem: this doesn't actually seem to update the user 'level' field when it should. What am I doing wrong?
// Define our user schema
var UserSchema = new mongoose.Schema({
...
points: {type: Number, default: 0, set: pointsChangeHandler},
level: {name: String, minPoints: Number, maxPoints: Number},
...
});
And the setter looks like so:
function goodPointsChangeHandler(newValue) {
var self = this;
if (newValue != self.goodPoints) {
console.log('Point change detected.');
// Find level that corresponds with new point total
Level.findOne({
'minPoints': {$lte : self.goodPoints},
'maxPoints': {$gt : self.goodPoints}}, '-_id').lean().exec(function(err, level) {
if (self.goodLevel == undefined || self.goodLevel.rank != level.rank) {
console.log('Level changed.');
self.goodLevel = level;
}
return newValue;
});
}
return newValue;
}
Based on #laggingreflex's comment, I tried modifying this within the scope of the model method (i.e. not in the Level.findOne() callback, and changes made that way were persisted without an explicit save() call.
Also, I had a pretty silly error where I was returning newValue from thefindOne` callback.. not sure what I was thinking there...
Long story short, and this may be obvious to node/express/mongoose experts, but you can modify fields other than the one whose setter method you're currently in, but the moment you find yourself in the callback of another async method, you'll have to do an explicit save() or your modifications to this will not be persisted.
So:
function myFieldSetterMethod(newValue) {
this.myField = "a";
this.myOtherField = "b";
return newValue;
// no save() necessary, this will update both fields
}
function myFieldSetterMethod(newValue) {
this.myField = "a";
SomeModel.findOne(..., function(err, doc) {
this.myOtherField = doc.somethingIWantFromThisDoc;
// now you'll need an explicit save
// this.save(...)
});
return newValue;
}

NodeJS Module Pattern

I'm about to begin writing a new module for a system I'm developing. We use a MySQL database (so I'm using node-mysql) which contains a customers table.
What I want to achieve is:
Outside of the module I'm looking to do var C = new Customer(1) where 1 is the customer ID.
Now when I want to get something from this customer, I can do C.email or C.first_name which will simply return a value.
I also need to be able to set values back on this customer, C.email = 'example#example.com' or perhaps:
C.set('email', 'example#example.com')
What would be the best pattern to create such a model?
I already have something like this... Not exactly what you demanded but very close to that
I have generalized the core part and here is the code..Hope this will help....
var mysql = require('mysql');
var con = mysql.createConnection({
host:"yourHostName",
user:"yourUserName",
password:"yourPassword"
});
con.query("use databaseName");
function getCustomerDetails(custId){
con.query("select * from customer where custId = "+custId,function(err,result,fields){
if(!err)
return result;
else
console.log(err);
});
}
function updateCustomerDetails(custId,fieldName,fieldValue){
con.query("update customer set "+fieldName+" = "+fieldValue+" where custId = "+custId,function(err,result,fields){
if(!err)
return true;
else
console.log(err);
return false;
});
}
exports.getCustomerDetails = getCustomerDetails;
exports.updateCustomerDetails = updateCustomerDetails;
And then suppose you saved the module as dbAccessModule.js Then you can use the functions like this
var C = require('./dbAccessModule');
result = C.getCustomerDetails(1);
console.log(result.fieldName);
var success = C.updateCustomerDetails(1,'name','sumit');
if(success)
console.log('Table Updated successfully....');
else
// take necessary action according to your application
One thing you need to take care of is that if you are updating any field with string value
then please don't forget to surround the value of fieldValue with single quotes.
If this is not what you asked for then please ignore it....
I recently created two database modules you might be interested in checking out to see if they fit your needs - an ORM: http://bookshelfjs.org and Query Builder: http://knexjs.org
The ORM is based off of the design patterns of Backbone.js
So, you'd be able to do something like this:
// Create the base customer object
var Customer = Bookshelf.Model.extend({
tableName: 'customers'
});
// Create a new customer instance with an id of 1, fetch it, and then
// act on the result model 'customer'.
new Customer({id: 1}).fetch().then(function(customer) {
console.log(customer.get('name'))
customer.set('email', 'email#example.com')
return customer.save();
});
You could also extend the base Customer class to enable a shortened syntax, similar to what you're looking for:
// Create the base customer object, with a static findOne method.
var Customer = Bookshelf.Model.extend({
tableName: 'customers'
}, {
find: function(id) {
return new this({id: id}).fetch();
}
});
Customer.find(1).then(function(C) {
console.log(C.get('name'))
C.set('email', 'email#example.com')
});

Resources