how to have a field that conditionally MUST be null? - node.js

This isn't quite a "sometimes required" field, but a little different.
I need to have a field in a Mongoose document that, depending on other data in the document, must NEVER be populated.
Is there a way to define the schema such that if field a is populated, that field b MUST be null?
I would strongly prefer to NOT solve this with mongoose hooks...

It can be done using custom validators, this example is for mongoose version 5.x.
new Schema({
a: {
type: String
},
b: {
type: String,
default: null,
validate: {
validator: function (v) {
if (this.a && v === null) {
return true
}
return false
},
message: (props) => `${props.value} should be null!`
}
}
})

Related

Sequelizejs "isAfter" validation with other field

In the Sequelize's docs, they have mention following way to restrict a date field value to after a certain date.
validate: {
isAfter: '2018-10-02'
}
But I want to perform this validation against another date field from req.body
Like
validate: {
isAfter: anotherFieldName // fieldname instead of static value
}
The field validators do not have access to the model instance's other properties. In order to validate that two values on an instance pass your validation check, You should make use of Sequelize's custom validate object in the model options:
const SomeModel = db.define(
'some_model',
{
start_date: {
type: Sequelize.DATEONLY,
validate: {
isDate: true
}
},
end_date: {
type: Sequelize.DATEONLY,
validate: {
isDate: true
}
},
},
{
validate: {
startDateAfterEndDate() {
if (this.start_date.isAfter(this.end_date)) {
throw new Error('Start date must be before the end date.');
}
}
}
}
);

GraphQLNonNull opposite

i`m trying implements graphql and i have problem.
I did type for graphql:
export const menuItemDataType = new GraphQL.GraphQLObjectType({
name: 'MenuItemData',
fields: () => ({
staticUrl: {
type: GraphQL.GraphQLString
},
page: {
type: new GraphQL.GraphQLNonNull(menuItemType),
resolve(MenuItemData) {
return PageRepository.getPageById(MenuItemData.page).exec();
}
},
menu: {
type: new GraphQL.GraphQLNonNull(menuType),
resolve(MenuItemData) {
return MenuRepository.getMenuById(MenuItemData.menu).exec();
}
}
})
})
and in this GraphQLObjectType i have page and menu.
I use mongoDB with mongoose. page and menu are nullable in model. When i query in graphql on this property so its chance that can be return null, but this is not compatible with GraphQL.GraphQLNonNull. Return error with "message": "Cannot return null for non-nullable field MenuItemData.page."
My question is: "Is any opposite for GraphQLNonNull. Like GraphQL?". I didnt found it.
Thank you
Don't use GraphQLNonNull if the type is nullable. GraphQL fields can have no value by default.
type: new GraphQL.GraphQLNonNull(menuType)
becomes
type: menuType

mongoose duplicate items getting inserted using $addToSet and $each to push items into the array

I am trying to push an array of objects into a document. I am using $addToSet to try and not insert duplicate data. I want to do a check on applied.studentId. But if I pass the same request twice, then the data is getting inserted. Is there any check on $addToSet and $each that I have to use?
My schema is as follows
jobId: { type: Number},
hiringCompanyId: String,
applied: [{
studentId: String,
firstName:String,
lastName:String,
gender:String,
identityType:String,
identityValue:String,
email:String,
phone:String,
}],
My node code is as follows.
public ApplyForJob(data: JobDto): Promise<{ status: string }> {
let students = data.applied;
let findQuery = {hiringCompanyId: data.hiringCompanyId, jobId: data.companyJobId};
let appliedQuery = {};
if (!isNullOrUndefined(data.applied.length)) {
appliedQuery = {
"$addToSet": {
"applied": {
"$each": data.applied
}
}
};
}
return new Promise((resolve, reject) => {
Jobs.findOneAndUpdate(findQuery, appliedQuery).exec((err, info) => {
if (err) {
reject(new UpdateError('Jobs - Update()', err, Jobs.collection.collectionName));
} else {
console.log(info);
resolve({status: "Success"});
}
})
});
}
On disabling the date field, $addToSet does not add duplicate values. As per the doc https://docs.mongodb.com/manual/reference/operator/update/addToSet/
As such, field order matters and you cannot specify that MongoDB compare only a subset of the fields in the document to determine whether the document is a duplicate of an existing array element.
as Rahul Ganguly mention absolutely correctly, we cannot use reliably $addToSet with JS objects.
One options is to move applied in to separate collection and make Job schema to ref new Applied model.
Example:
{
jobId: { type: Number },
hiringCompanyId: String,
applied: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Applied'
}]
}

Extra properties on Mongoose schema field?

Is it possible to add extra / custom attributes to the field in a Mongoose schema? For example, note the name: attribute on the following fields:
var schema = mongoose.Schema({
_id : { type: String, default: $.uuid.init },
n : { type: String, trim: true, name: 'name' },
ac : { type: Date, required: true, name: 'created' },
au : { type: Date, name: 'updated' },
ad : { type: Date, name: 'deleted' },
am : { type: String, ref: 'Member', required: true, name: 'member' }
});
We expect to have a large number of docs in our system and would like to conserve as much space as possible. In this example, we have abbreviated the name of the fields (n vs name, etc.). We would like to use the additional name field to hydrate a JSON object after a fetch.
You could create an instance method (which I called toMappedObject, but you're free to name it however you like) that could perform the conversion by checking the schema for each field to see if it has a name property:
schema.methods.toMappedObject = function() {
let obj = this.toObject();
Object.keys(obj).forEach(fieldName => {
let field = schema.tree[fieldName];
if (field.name) {
obj[field.name] = obj[fieldName];
delete obj[fieldName];
}
});
return obj;
}
// Example usage:
let doc = new Model({...});
let obj = doc.toMappedObject();
Alternatively, you can configure your schema to automatically transform the output generated by toJSON, although it's much more implicit so easy to overlook in case issues pop up (I haven't tested this very well):
schema.set('toJSON', {
transform : function(doc, obj) {
Object.keys(obj).forEach(fieldName => {
let field = doc.schema.tree[fieldName];
if (field.name) {
obj[field.name] = obj[fieldName];
delete obj[fieldName];
}
});
return obj;
}
});
// Example usage:
let doc = new Model({...});
console.log('%j', doc); // will call `doc.toJSON()` implicitly

mongoose subdocument sorting

I have an article schema that has a subdocument comments which contains all the comments i got for this particular article.
What i want to do is select an article by id, populate its author field and also the author field in comments. Then sort the comments subdocument by date.
the article schema:
var articleSchema = new Schema({
title: { type: String, default: '', trim: true },
body: { type: String, default: '', trim: true },
author: { type: Schema.ObjectId, ref: 'User' },
comments: [{
body: { type: String, default: '' },
author: { type: Schema.ObjectId, ref: 'User' },
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
}],
tags: { type: [], get: getTags, set: setTags },
image: {
cdnUri: String,
files: []
},
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
});
static method on article schema: (i would love to sort the comments here, can i do that?)
load: function (id, cb) {
this.findOne({ _id: id })
.populate('author', 'email profile')
.populate('comments.author')
.exec(cb);
},
I have to sort it elsewhere:
exports.load = function (req, res, next, id) {
var User = require('../models/User');
Article.load(id, function (err, article) {
var sorted = article.toObject({ getters: true });
sorted.comments = _.sortBy(sorted.comments, 'created_at').reverse();
req.article = sorted;
next();
});
};
I call toObject to convert the document to javascript object, i can keep my getters / virtuals, but what about methods??
Anyways, i do the sorting logic on the plain object and done.
I am quite sure there is a lot better way of doing this, please let me know.
I could have written this out as a few things, but on consideration "getting the mongoose objects back" seems to be the main consideration.
So there are various things you "could" do. But since you are "populating references" into an Object and then wanting to alter the order of objects in an array there really is only one way to fix this once and for all.
Fix the data in order as you create it
If you want your "comments" array sorted by the date they are "created_at" this even breaks down into multiple possibilities:
It "should" have been added to in "insertion" order, so the "latest" is last as you note, but you can also "modify" this in recent ( past couple of years now ) versions of MongoDB with $position as a modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": { "comments": { "$each": [newComment], "$position": 0 } }
},
function(err,result) {
// other work in here
}
);
This "prepends" the array element to the existing array at the "first" (0) index so it is always at the front.
Failing using "positional" updates for logical reasons or just where you "want to be sure", then there has been around for an even "longer" time the $sort modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": {
"comments": {
"$each": [newComment],
"$sort": { "$created_at": -1 }
}
}
},
function(err,result) {
// other work in here
}
);
And that will "sort" on the property of the array elements documents that contains the specified value on each modification. You can even do:
Article.update(
{ },
{
"$push": {
"comments": {
"$each": [],
"$sort": { "$created_at": -1 }
}
}
},
{ "multi": true },
function(err,result) {
// other work in here
}
);
And that will sort every "comments" array in your entire collection by the specified field in one hit.
Other solutions are possible using either .aggregate() to sort the array and/or "re-casting" to mongoose objects after you have done that operation or after doing your own .sort() on the plain object.
Both of these really involve creating a separate model object and "schema" with the embedded items including the "referenced" information. So you could work upon those lines, but it seems to be unnecessary overhead when you could just sort the data to you "most needed" means in the first place.
The alternate is to make sure that fields like "virtuals" always "serialize" into an object format with .toObject() on call and just live with the fact that all the methods are gone now and work with the properties as presented.
The last is a "sane" approach, but if what you typically use is "created_at" order, then it makes much more sense to "store" your data that way with every operation so when you "retrieve" it, it stays in the order that you are going to use.
You could also use JavaScript's native Array sort method after you've retrieved and populated the results:
// Convert the mongoose doc into a 'vanilla' Array:
const articles = yourArticleDocs.toObject();
articles.comments.sort((a, b) => {
const aDate = new Date(a.updated_at);
const bDate = new Date(b.updated_at);
if (aDate < bDate) return -1;
if (aDate > bDate) return 1;
return 0;
});
As of the current release of MongoDB you must sort the array after database retrieval. But this is easy to do in one line using _.sortBy() from Lodash.
https://lodash.com/docs/4.17.15#sortBy
comments = _.sortBy(sorted.comments, 'created_at').reverse();

Resources