How to get enum values from mongoose schema using virtual method? - node.js

I'm having difficulty getting enum values from my Mongoose schema using a virtual method on that same schema.
The property I'm trying to access in the schema is defined as follows:
, roles: {
type: [{
type: String
, enum: ['user', 'admin']
}]
, default: ['user']
}
The following is my virtual method I'm using to grab the enum values:
// Returns an array of all possible role enum values
UserSchema.virtual('possibleRoles').get(function() {
return this.schema.path('roles').caster.enumValues;
});
This works, however other examples I found online went about it in a different way. An example of this is here: Access the list of valid values for an Enum field in a Mongoose.js Schema
Is my method for accessing enums on a property dirty or incorrect? Is there a cleaner way I could write this?

This is clean and easy way.
var possibleRoles = ['user', 'admin'];
var UserSchema = new Schema({
roles: {
type: [{type: String, enum: possibleRoles}],
default: ['user']
}
});
UserSchema.virtual('possibleRoles').get(function () {
return possibleRoles;
});

remove the caster part, i don't know why that is there:
return this.schema.path('roles').enumValues;
that should work without any other issues

Related

Using TypeScript enum with mongoose schema

I have a schema with an enum:
export interface IGameMapModel extends IGameMap, Document {}
export const gameMapSchema: Schema = new Schema({
name: { type: String, index: { unique: true }, required: true },
type: { type: String, enum: CUtility.enumToArray(GameMode) }
});
export const GameMap: Model<IGameMapModel> = model<IGameMapModel>('GameMap', gameMapSchema);
The GameMap is an enum.
First problem is already in here: I need to convert the enum to a string array in order to use it with the schema.
Secondly, I wanna use an enum value directly during the schema creation.
new GameMap({
name: 'Test',
type: GameMode.ASSAULT
});
returns ValidationError: type: '1' is not a valid enum value for path 'type'.
I am not sure whether this can actually work due to the string array I set in the model enum property.
My idea would be to create some kind of type conversion during the schema creation. Does this work with mongoose or would I have to create some kind of helper for object creation?
GameMode.ASSAULT is evaluating as it's numeric value, but GameMode is expecting the type to be a string. What are you expecting the string evaluation to be? If you need the string value of the enum, you can access it with GameMode[GameMode.ASSAULT], which would return ASSAULT as a string.
For example:
enum TEST {
test1 = 1,
test2 = 2
}
console.log(TEST[TEST.test1]);
//Prints "test1"
From the Mongoose docs on validation, in schema properties with a type of String that have enum validation, the enum that mongoose expects in an array of strings.
This means that CUtility.enumToArray(GameMode) needs to either return to you an array of the indexes as strings, or an array of the text/string values of the enum--whichever you are expecting to store in your DB.
The validation error seems to imply that 1 is not contained within the array that is being produced by CUtility.enumToArray(GameMode), or the validation is seeing GameMode.ASSAULT as a number when it is expected a string representation of 1. You might have to convert the enum value you are passing in into a string.
What is the output of CUtility.enumToArray(GameMode)? That should help you determine which of the two is your problem.
Why don't you just create custom getter/setter:
const schema = new Schema ({
enumProp: {
type: Schema.Types.String,
enum: enumKeys(EnumType),
get: (enumValue: string) => EnumType[enumValue as keyof typeof EnumType],
set: (enumValue: EnumType) => EnumType[enumValue],
},
});
EDIT:
Don't forget to explicitly enable the getter
schema.set('toJSON', { getters: true });
// and/or
schema.set('toObject', { getters: true });
This way you can fine-control how exactly you want to represent the prop in the db, backend and frontend (json response).

Handling Mongoose Populated Fields in GraphQL

How do I represent a field that could be either a simple ObjectId string or a populated Object Entity?
I have a Mongoose Schema that represents a 'Device type' as follows
// assetSchema.js
import * as mongoose from 'mongoose'
const Schema = mongoose.Schema;
var Asset = new Schema({ name : String,
linked_device: { type: Schema.Types.ObjectId,
ref: 'Asset'})
export AssetSchema = mongoose.model('Asset', Asset);
I am trying to model this as a GraphQLObjectType but I am stumped on how to allow the linked_ue field take on two types of values, one being an ObjectId and the other being a full Asset Object (when it is populated)
// graphql-asset-type.js
import { GraphQLObjectType, GraphQLString } from 'graphql'
export var GQAssetType = new GraphQLObjectType({
name: 'Asset',
fields: () => ({
name: GraphQLString,
linked_device: ____________ // stumped by this
});
I have looked into Union Types but the issue is that a Union Type expects fields to be stipulated as part of its definition, whereas in the case of the above, there are no fields beneath the linked_device field when linked_device corresponds to a simple ObjectId.
Any ideas?
As a matter of fact, you can use union or interface type for linked_device field.
Using union type, you can implement GQAssetType as follows:
// graphql-asset-type.js
import { GraphQLObjectType, GraphQLString, GraphQLUnionType } from 'graphql'
var LinkedDeviceType = new GraphQLUnionType({
name: 'Linked Device',
types: [ ObjectIdType, GQAssetType ],
resolveType(value) {
if (value instanceof ObjectId) {
return ObjectIdType;
}
if (value instanceof Asset) {
return GQAssetType;
}
}
});
export var GQAssetType = new GraphQLObjectType({
name: 'Asset',
fields: () => ({
name: { type: GraphQLString },
linked_device: { type: LinkedDeviceType },
})
});
Check out this excellent article on GraphQL union and interface.
I was trying to solve the general problem of pulling relational data when I came across this article. To be clear, the original question appears to be how to dynamically resolve data when the field may contain either the ObjectId or the Object, however I don't believe it's good design in the first place to have a field store either object or objectId. Accordingly, I was interested in solving the simplified scenario where I keep the fields separated -- one for the Id, and the other for the object. I also, thought employing Unions was overly complex unless you actually have another scenario like those described in the docs referenced above. I figured the solution below may interest others also...
Note: I'm using graphql-tools so my types are written schema language syntax. So, if you have a User Type that has fields like this:
type User {
_id: ID
firstName: String
lastName: String
companyId: ID
company: Company
}
Then in my user resolver functions code, I add this:
User: { // <-- this refers to the User Type in Graphql
company(u) { // <-- this refers to the company field
return User.findOne({ _id: u.companyId }); // <-- mongoose User type
},
}
The above works alongside the User resolver functions already in place, and allow you write GQL queries like this:
query getUserById($_id:ID!)
{ getUserById(_id:$_id) {
_id
firstName
lastName
company {
name
}
companyId
}}
Regards,
S. Arora

Mongoose Schema enum validation on array of strings

The enum validation works when it's a single String:
var songSchema = new Schema({
vocalRange: {
type: String,
enum: ["Soprano", "Mezzo-soprano", "Contralto", "Alto", "Tenor", "Baritone", "Bass"],
}
})
Trying to POST anything but what's in the enum returns a validation error through mongoose.
However, the following:
var songSchema = new Schema({
vocalRange: {
type: [String], // this is what changed
enum: ["Soprano", "Mezzo-soprano", "Contralto", "Alto", "Tenor", "Baritone", "Bass"],
}
})
Allows me to POST anything for vocalRange without any kind of validation. What's going on? And why doesn't the validation work for an array of strings like it does for a single string?
Answering from my comment:
vocalRange: [{ type: String }]
should work.
As of mongoose version 5.0.6 and higher, the OP issue now works!
vocalRange: {
type: [String], // this now works!
enum: ["Soprano", "Mezzo-soprano", "Contralto", "Alto", "Tenor", "Baritone", "Bass"],
}
Reference
https://github.com/Automattic/mongoose/issues/6204#issuecomment-374690551

Query Parse.com migrated database pointer relationship with Mongoose

Context
So we have migrated from Parse.com to an hosted MongoDB database. Now I have to write a script that queries our database directly (not using Parse).
I'm using nodejs / mongoose and am able to retrieve these documents.
Problem
Here is my schema so far:
var StorySchema = new mongoose.Schema({
_id: String,
genre: String
});
var ActivitySchema = new mongoose.Schema({
_id: String,
action: String,
_p_story: String /* Also tried: { type: mongoose.Schema.Types.ObjectId, ref: 'Story' } and { type: String, ref: 'Story' }*/,
});
I would like to write a query that fetches theses documents with the related Story (stored as a pointer).
Activity
.find({
action: 'read',
})
.exec(function(error, activities) {
activities.forEach(function(activity) {
// I would like to use activity._p_story or whatever the mean to access the story here
});
});
Question
Is there a way to have the fetched activities populated with their story, given that the _p_story field contains Story$ before the object id?
Thanks!
One option I have been looking at is the ability to create a custom data type for each pointer. The unfortunate side is Parse treats these as 'belongsTo' relationships and but does not store the 'hasMany' relationship that Mongoose wants for populate(). But once this is in place you can easily do loops to get the relational data. Not ideal but works and is what populate is really doing under the hood anyways.
PointerTypeClass.js -> This would work for populating the opposite direction.
var Pointer = function(mongoose) {
function PointerId(key, options) {
mongoose.SchemaType.call(this, key, options, 'PointerId');
}
PointerId.prototype = Object.create(mongoose.SchemaType.prototype);
PointerId.prototype.cast = function(val) {
return 'Pointer$' + val;
}
return PointerId;
}
module.exports = Pointer;
Also be sure mongoose knows about the new type by doing mongoose.Schema.Types.PointerId = require('./types/PointerTypeClass')(mongoose);
Lastly. If you are willing to write some cloudcode you could create the array of ids for your populate to know about the objects. Basically in your Object.beforeSave you would update the array of the id for the relationship. Hope this helps.

Mongoose variable key name

I have a mongo object and wish to access it via mongoose for my web app. The schema I've defined has an Object storing user ids and a 3-level value (yes, maybe or no).
e.g.
"user_info": {
"<id_value_1>": "y",
"<id_value_2>": "n"
}
The id_value_*s above are the users session ids so a long string of random characters. How can I create a mongoose Schema for this?
Would user_info: {String, String} work?
I could restructure it so that the user_info is an array of objects { "sessionid": "<value>", "value: "y"}, which would be ok, is this the best option?
You'll be better off if you avoid dynamic keys in your schema and go with your second idea of:
user_info: [{sessionid: String, value: String}]
You can use the $ positional operator to update individual user_info array elements by sessionid.
You may try with Schema Type Mixed like this way
var user = new Schema({
info: [Schema.Types.Mixed]
});
user.info = { any: { thing: 'i want' } };
user.markModified('info');
You can read more about it here
After testing the above, I found that defining the schema as user_info: { String: String } is a valid way to do this (option 1 specified in the question).
You may define objects and arrays in your schema. You may even combine them. For example, this is an array of objects:
var user = new Schema({
foo: [ {
address: {type: String},
email: {type: String, unique: true}
}],
bar: [ "simple", "array" ]
});

Resources