mongoose can't store double types? - mongodb

I am trying to store double BSON type inside mongodb. I am using mongoose. I have tried every possible way but still it's stored as int.
I have tried #mongoosejs/double and mongoose-float but none of them work.
await Variant.insertOne(
{
price: 345,
discount: 10,
},
)
product model
import { Schema } from "mongoose"
const Double = require("#mongoosejs/double")
// const Float = require("mongoose-float").loadType(mongoose)
export const ProductVariantEmbeddedSchema = new Schema({
price: Double,
discount: Double,
})
here is a custom type that I have created by the help of #mongoosejs/double.
import mongoose from "mongoose"
export default function mongooseDouble(mongoose) {
class DoubleType extends Number {
constructor(v) {
super(v)
this.value = v
}
toBSON() {
return this.value
}
}
class Double extends mongoose.SchemaType {
constructor(key, options) {
super(key, options, "Double")
Object.assign(this.$conditionalHandlers, {
$lt: (val) => this.castForQuery(val),
$lte: (val) => this.castForQuery(val),
$gt: (val) => this.castForQuery(val),
$gte: (val) => this.castForQuery(val),
})
}
cast(val) {
if (val == null) {
return val
}
const _val = Number(val)
if (isNaN(_val)) {
throw new mongoose.SchemaType.CastError(
"Double",
val + " is not a valid double"
)
}
return new DoubleType(_val)
}
}
mongoose.Schema.Types.Double = Double
mongoose.Types.Double = DoubleType
return mongoose
}
// export default Double

I believe there is no such type as Double. JavaScript has Number which supports int, float, double, etc. Also, if you see mongoose documentation then you will see Double is not a valid type. Instead, you should use Number.
const ProductVariantEmbeddedSchema = new Schema({
price: Number,
discount: Number,
});
EDIT: After discussion in the comment I believe this can be a workaround.
const price = 5;
await Model.create({
price: price * 1.0001,
...
});
In the database, the price would be a double type but the value would be 5.0005. So, whenever you want the use the value of price either make it int or use .toFixed(2) or similar function to limit the decimal point to 2 places.

ok it seems that if mongoose custom types not worked I can use raw mongodb queries.
import mongoose, {mongo} from 'mongoose'
const result = await mongoose.connection.collection('Variant').insertMany([{price: new mongo.Double(34)}, {price: new mongo.Double(45)}])
const storedValues = result.opt

Another solution that works :
const mongoose = require('mongoose');
const setter = (value: any) => {
const result = mongoose.Types.Decimal128.fromString(parseFloat(value).toFixed(2));
result._bsontype = 'Double'; // solution
return result;
};
const Double = new mongoose.Schema(
{
anyDoubleValue: {
type: mongoose.SchemaTypes.Mixed,
set: setter
}
},
{
collection: 'double'
}
);
export = mongoose.model('DoubleModel', Double);
You can manually change the _bsonType properties to Double

Related

How to define ObjectId type of dynamic object's properties in mongoose schema?

The current schema looks like this:
const schema = new mongoose.Schema(
filters: {
type: Object,
default: {}
}
);
Sample Input Document:
{
filters: {
field1: "5a934e000102030405000001",
field2: "5a934e000102030405000002",
field3: "5a934e000102030405000003"
}
}
The filters object property will store dynamic properties/fields but all will be ObjectId type,
Currently, I am using pre-hook and lodash methods to convert type into ObjectId type,
SchemaObj.pre('save', function (next) {
this.filters = _.reduce(this.filters, (r, v, k) => v ? { ...r, [k]: mongoose.Types.ObjectId(v) } : r, {});
next();
});
Is there any way to define ObjectId type of dynamic object fields?
I am expecting something like this if mongoose provide any other approach:
const schema = new mongoose.Schema(
filters: {
type: {
*: mongoose.Types.ObjectId
}
}
);
You can define the field as a Map
const schema = new mongoose.Schema(
filters: {
type: Map,
of: mongoose.Schema.Types.ObjectId
}
);

Why is mongoose not allowing me to find by $in on _id unless I have a schema wrapping another schema?

Please, if anyone can help explain the below situation. I've lost hours only to discover a working solution but it doesn't make sense why it needs to be implemented this way.
For some reason if I connect mongo over nodejs, using 1 mongoose schema, I cannot search via $in.
When I wrap that single schema in another schema, with no additional information $in returns a match.
Note: the command line works always. So it is something to do with the way the schema needs to be impelemnted.
I am new to mongoose so maybe there is something obvious i am missing but it does not make any sense. Any help appreciated.
thanks
creation of data in docker
Note there is only one record:
#!/bin/bash
set -e
mongo <<EOF
use ${MONGO_DB_NAME}
db.createCollection("persons")
db.persons.insert({"name": "CPU"})
EOF
schema.ts
import { Model, Schema, Document, Connection } from 'mongoose';
interface IPerson {
_id: string;
name: string;
};
type TSchemaPerson = Schema<IPerson>;
const SchemaPerson: TSchemaPerson = new Schema({
_id: String,
name: String
});
// ##########################################################################
// ODD BEHAVIOUR ?
// I have to wrap SchemaPerson inside SchemaPersons,
// even though its basically a duplication in order to get $in to return a result
// --------------------------------------------------------
// \/
const SchemaPersons: Schema<TSchemaPerson> = new Schema({SchemaPerson});
// ##########################################################################
const collectionName: string = 'Person';
export type TDocumentPerson = Model<Document & IPerson>;
export type TConnPersons = (con: Connection) => TDocumentPerson;
// ##########################################################################
// ODD BEHAVIOUR ?
// If I return SchemaPerson instead of SchemaPersons here then $in does not return a result --------
// \/
const ModelPersons: TConnPersons = con => con.model(collectionName, SchemaPersons);
// ##########################################################################
export default ModelPersons;
graphql - resolver.ts
const persons: QueryResolvers['persons'] = async (parent, args, context) => {
console.log('persons()');
const { dbConn }: IContext = context;
if (dbConn) {
const Person: TDocumentPerson = ModelPersons(dbConn);
let result;
let resultIn;
try {
result = await Person.find().exec();
console.log('result = ', result); // Result in both cases = [ { _id: 5f36e199ebd39f53b437834b, name: 'CPU' } ]
resultIn = await Person.find({
_id: {
$in: [new ObjectId('5f36e199ebd39f53b437834b')]
}
}).exec();
// ##########################################################################
// ODD BEHAVIOUR ?
// Result with SchemaPersons = [ { _id: 5f36e199ebd39f53b437834b, name: 'CPU' } ]
// Result with SchemaPerson = [ ]
console.log('resultIn = ', resultIn);
// ##########################################################################
} catch (err) {
console.log('err = ', err);
}
}
};
Note if I change the model to a collection then it does work. See below:
schema.ts - connecting as collection
import { Schema, Collection, Connection } from 'mongoose';
interface IPerson {
_id: string;
name: string;
};
type TSchemaPerson = Schema<IPerson>;
const SchemaPerson: TSchemaPerson = new Schema({
_id: String,
name: String
});
export type TCollectionPersons = Collection & IPerson;
export type TConnPersons = (con: Connection) => TCollectionPersons;
const CollectionPersons: TConnPersons = con => con.collection('persons', SchemaPerson)
export default CollectionPersons;
resolver.ts
const persons: TCollectionPersons = CollectionPersons(dbConn);
let result;
let resultIn;
let resultById
try {
result = await persons.find().toArray();
console.log('result = ', result);
// Result [ { _id: 5f36e199ebd39f53b437834b, name: 'CPU' } ]
resultIn = await persons.find({
_id: {
$in: [new ObjectId('5f36e199ebd39f53b437834b')]
}
}).toArray();
console.log('resultIn = ', resultIn);
// Result [ { _id: 5f36e199ebd39f53b437834b, name: 'CPU' } ]
// cannot use findById on collection.
// resultById = await persons.findById('5f36e199ebd39f53b437834b');
} catch (err) {
console.log('err = ', err);
}
}

MongoDB query - pass the function to the Model.find()

I have issue with querying MongoDB (Mongoose) by passing the function as parameter in Model.find() -> like this Model.find(searchCondition). I hope that you can help me.
// Fetching patients from the database
exports.getPatients = (req, res, next) => {
const criterionSearchCategory = req.query.kriterijumPretrage;
const ageSearchCategory = req.query.kriterijumGodina;
const searchInputValue = req.query.pojamPretrage;
console.log({ [criterionSearchCategory]: { [ageSearchCategory]: Number([searchInputValue]) }});
// Patient search condition, based on selected option from select dropdown
function searchCondition() {
if (criterionSearchCategory == 'undefined') {
return {};
} else if (criterionSearchCategory == 'age') {
return { [criterionSearchCategory]: { [ageSearchCategory] : Number([searchInputValue]) }}
} else {
return { [criterionSearchCategory]: { $in: [ "/^" + searchInputValue + "/i" ]}}
}
}
...
const patientQuery = Patient.find(searchCondition);
getPatients(patientsPerPage: number, currentPage: number, criterionSearchCategory: string, searchInputValue: string, ageSearchCategory: any) {
const queryParams = `?pacijenataPoStranici=${patientsPerPage}&trenutnaStranica=${currentPage}&kriterijumPretrage=${criterionSearchCategory}&pojamPretrage=${searchInputValue}&kriterijumGodina=${ageSearchCategory}`;
this.http
.get<{ message: string, patients: any, maxPatients: number }>( BACKEND_URL + queryParams)
// Execute map on every data that makes it through Observable stream
.pipe(map((patientData) => {
I want to menton when I pass the query params manually, for example const patientQuery = Patient.find({ age: { '$gt': 30 } }); appropriate patients will be fetched correctly , but when I pass the function , like this const patientQuery = Patient.find(searchCondition); then does not work.
The first question, is it possible to pass the function as parameter like this?
Any suggestion will be appreciate. Thank you

apollostack/graphql-server - how to get the fields requested in a query from resolver

I am trying to figure out a clean way to work with queries and mongdb projections so I don't have to retrieve excessive information from the database.
So assuming I have:
// the query
type Query {
getUserByEmail(email: String!): User
}
And I have a User with an email and a username, to keep things simple. If I send a query and I only want to retrieve the email, I can do the following:
query { getUserByEmail(email: "test#test.com") { email } }
But in the resolver, my DB query still retrieves both username and email, but only one of those is passed back by apollo server as the query result.
I only want the DB to retrieve what the query asks for:
// the resolver
getUserByEmail(root, args, context, info) {
// check what fields the query requested
// create a projection to only request those fields
return db.collection('users').findOne({ email: args.email }, { /* projection */ });
}
Of course the problem is, getting information on what the client is requesting isn't so straightforward.
Assuming I pass in request as context - I considered using context.payload (hapi.js), which has the query string, and searching it through various .split()s, but that feels kind of dirty. As far as I can tell, info.fieldASTs[0].selectionSet.selections has the list of fields, and I could check for it's existence in there. I'm not sure how reliable this is. Especially when I start using more complex queries.
Is there a simpler way?
In case you don't use mongDB, a projection is an additional argument you pass in telling it explicitly what to retrieve:
// telling mongoDB to not retrieve _id
db.collection('users').findOne({ email: 'test#test.com' }, { _id: 0 })
As always, thanks to the amazing community.
2020-Jan answer
The current answer to getting the fields requested in a GraphQL query, is to use the graphql-parse-resolve-info library for parsing the info parameter.
The library is "a pretty complete solution and is actually used under the hood by postgraphile", and is recommended going forward by the author of the other top library for parsing the info field, graphql-fields.
Use graphql-fields
Apollo server example
const rootSchema = [`
type Person {
id: String!
name: String!
email: String!
picture: String!
type: Int!
status: Int!
createdAt: Float
updatedAt: Float
}
schema {
query: Query
mutation: Mutation
}
`];
const rootResolvers = {
Query: {
users(root, args, context, info) {
const topLevelFields = Object.keys(graphqlFields(info));
return fetch(`/api/user?fields=${topLevelFields.join(',')}`);
}
}
};
const schema = [...rootSchema];
const resolvers = Object.assign({}, rootResolvers);
// Create schema
const executableSchema = makeExecutableSchema({
typeDefs: schema,
resolvers,
});
Sure you can. This is actually the same functionality that is implemented on join-monster package for SQL based db's. There's a talk by their creator: https://www.youtube.com/watch?v=Y7AdMIuXOgs
Take a look on their info analysing code to get you started - https://github.com/stems/join-monster/blob/master/src/queryASTToSqlAST.js#L6-L30
Would love to see a projection-monster package for us mongo users :)
UPDATE:
There is a package that creates a projection object from info on npm: https://www.npmjs.com/package/graphql-mongodb-projection
You can generate MongoDB projection from info argument. Here is the sample code that you can follow
/**
* #description - Gets MongoDB projection from graphql query
*
* #return { object }
* #param { object } info
* #param { model } model - MongoDB model for referencing
*/
function getDBProjection(info, model) {
const {
schema: { obj }
} = model;
const keys = Object.keys(obj);
const projection = {};
const { selections } = info.fieldNodes[0].selectionSet;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const isSelected = selections.some(
selection => selection.name.value === key
);
projection[key] = isSelected;
}
console.log(projection);
}
module.exports = getDBProjection;
With a few helper functions you can use it like this (typescript version):
import { parceGqlInfo, query } from "#backend";
import { GraphQLResolveInfo } from "graphql";
export const user = async (parent: unknown, args: unknown, ctx: unknown, info: GraphQLResolveInfo): Promise<User | null> => {
const { dbQueryStr } = parceGqlInfo(info, userFields, "id");
const [user] = await query(`SELECT ${dbQueryStr} FROM users WHERE id=$1;`, [1]);
return user;
};
Helper functions.
Few points:
gql_uid used as ID! string type from primary key to not change db types
required option is used for dataloaders (if field was not requested by user)
allowedFields used to filter additional fields from info like '__typename'
queryPrefix is used if you need to prefix selected fields like select u.id from users u
const userFields = [
"gql_uid",
"id",
"email"
]
// merge arrays and delete duplicates
export const mergeDedupe = <T>(arr: any[][]): T => {
// #ts-ignore
return ([...new Set([].concat(...arr))] as unknown) as T;
};
import { parse, simplify, ResolveTree } from "graphql-parse-resolve-info";
import { GraphQLResolveInfo } from "graphql";
export const getQueryFieldsFromInfo = <Required = string>(info: GraphQLResolveInfo, options: { required?: Required[] } = {}): string[] => {
const { fields } = simplify(parse(info) as ResolveTree, info.returnType) as { fields: { [key: string]: { name: string } } };
let astFields = Object.entries(fields).map(([, v]) => v.name);
if (options.required) {
astFields = mergeDedupe([astFields, options.required]);
}
return astFields;
};
export const onlyAllowedFields = <T extends string | number>(raw: T[] | readonly T[], allowed: T[] | readonly T[]): T[] => {
return allowed.filter((f) => raw.includes(f));
};
export const parceGqlInfo = (
info: GraphQLResolveInfo,
allowedFields: string[] | readonly string[],
gqlUidDbAlliasField: string,
options: { required?: string[]; queryPrefix?: string } = {}
): { pureDbFields: string[]; gqlUidRequested: boolean; dbQueryStr: string } => {
const fieldsWithGqlUid = onlyAllowedFields(getQueryFieldsFromInfo(info, options), allowedFields);
return {
pureDbFields: fieldsWithGqlUid.filter((i) => i !== "gql_uid"),
gqlUidRequested: fieldsWithGqlUid.includes("gql_uid"),
dbQueryStr: fieldsWithGqlUid
.map((f) => {
const dbQueryStrField = f === "gql_uid" ? `${gqlUidDbAlliasField}::Text AS gql_uid` : f;
return options.queryPrefix ? `${options.queryPrefix}.${dbQueryStrField}` : dbQueryStrField;
})
.join(),
};
};

Decimal / Float in mongoose for node.js

I start my first test app on node.js / mongoDB / mongoose, this is a very simple app that aims to crate record in DB and retrieve them.
I create a model like:
var Car = new Schema({
brand : String,
speed : Number,
date : { type: Date, default: Date.now }
});
This is working fine, except that I would like to be able to provide a float value for speed instead of the integer one. I gave a try to Decimal and Float but none of them are working.
I did not find in the documentation either.
Any idea ?
I've searched a bit and found this article stating that for storing float values you must use Number type. You can store any float value in speed field.
Yes you can use the Decimal128 type.
https://mongoosejs.com/docs/api.html#mongoose_Mongoose-Decimal128
You can use the Decimal128 in Mongoose Schema as
speed:{
type:mongoose.Types.Decimal128
}
you can create your custom one. like so
'use strict';
const mongoose = require('mongoose');
class DoubleType extends Number {
constructor(v) {
super(v);
this.value = v;
this._bsontype = 'Double';
}
toBSON() {
return this;
}
}
class Double extends mongoose.SchemaType {
constructor(key, options) {
super(key, options, 'Double');
Object.assign(this.$conditionalHandlers, {
'$lt': val => this.castForQuery(val),
'$lte': val => this.castForQuery(val),
'$gt': val => this.castForQuery(val),
'$gte': val => this.castForQuery(val),
});
}
cast(val) {
if (val == null) {
return val;
}
if (val._bsontype === 'Double') {
return new DoubleType(val.value);
}
const _val = Number(val);
if (isNaN(_val)) {
throw new mongoose.SchemaType.CastError('Double',
val + ' is not a valid double');
}
return new DoubleType(_val);
}
}
mongoose.Schema.Types.Double = Double;
mongoose.Types.Double = DoubleType;
module.exports = Double;
source is copied from #mongoosejs/double
While the mongoDB fully supports float type, the mongoose supports only type of Number which is integer. If you try to save to mongoDB float number using mongooses type of Number it will be converted to string.
To sort this out, you will need to load some plugin for mongoose which will extend its value types. There are some plugins which work best with currencies or dates, but in your case I would use https://www.npmjs.com/package/mongoose-double.
Your model after changes would look something like this:
var mongoose = require('mongoose')
require('mongoose-double')(mongoose);
var SchemaTypes = mongoose.Schema.Types;
var Car = new Schema({
brand: {
type: String
},
speed: {
type: SchemaTypes.Double
},
date: {
type: Date,
default: Date.now
}
});
Hope it helps.