Missing Subdocument Methods in Mongoose with Typescript - mongodb

I'm working on a project and need to retrieve specific subdocuments from a model by their subdocument _id's. I then plan on making updates to those subdocuments and saving the main document. The mongoose subdocument documentation lists a number of methods you can call on the parent.children array, but methods that don't already exist in Javascript for arrays give an error saying they do not exist and it doesn't compile. I'm referencing this documentation: https://mongoosejs.com/docs/subdocs.html
I understand that should be able to use .findOneAndUpdate to make my updates, and using the runValidators option everything should still be validated, but I would also like to just retrieve the subdocument itself as well.
I looked at this post: MongoDB, Mongoose: How to find subdocument in found document? , and I will comment that the answer is incorrect that if a subdocument schema is registered it automatically creates a collection for that schema, the collection is only made if that schema is saved separately. You cannot use ChildModel.findOne() and retrieve a subdocument, as the collection does not exist, there is nothing in it.
Having IChildModel extend mongoose.Types.Subdocument and having the IParent interface reference that instead of IChild and not registering the ChildModel does not change anything other than no longer allowing calls to .push() to not accept simple objects (missing 30 or so properties). Also trying mongoose.Types.Array<IChild> in the IParent interface with this method does not change anything.
Changing the IParent interface to use mongoose.Types.Array<IChild> for the children property allows addToSet() to work, but not id() or create()
I'm using Mongoose version 5.5.10, MongoDb version 4.2.0 and Typescript version 3.4.5
import mongoose, { Document, Schema } from "mongoose";
// Connect to mongoDB with mongoose
mongoose.connect(process.env.MONGO_HOST + "/" + process.env.DB_NAME, {useNewUrlParser: true, useFindAndModify: false});
// Interfaces to be used throughout application
interface IParent {
name: string;
children: IChild[];
}
interface IChild {
name: string;
age: number;
}
// Model interfaces which extend Document
interface IParentModel extends IParent, Document { }
interface IChildModel extends IChild, Document { }
// Define Schema
const Child: Schema = new Schema({
name: {
type: String,
required: true
},
age: {
type: Number,
required: true
}
});
const ChildSchema: Schema = Child;
const Parent: Schema = new Schema({
name: {
type: String,
required: true
},
children: [ChildSchema]
});
const ParentSchema: Schema = Parent;
// Create the mongoose models
const ParentModel = mongoose.model<IParentModel>("Parent", Parent);
const ChildModel = mongoose.model<IChildModel>("Child", Child);
// Instantiate instances of both models
const child = new ChildModel({name: "Lisa", age: 7});
const parent = new ParentModel({name: "Steve", children: [child]});
const childId = child._id;
// Try to use mongoose subdocument methods
const idRetrievedChild = parent.children.id(childId); // Property 'id' does not exist on type 'IChild[]'.ts(2339)
parent.children.addToSet({ name: "Liesl", age: 10 }); // Property 'addToSet' does not exist on type 'IChild[]'.ts(2339)
parent.children.create({ name: "Steve Jr", age: 2 }); // Property 'create' does not exist on type 'IChild[]'.ts(2339)
// If I always know the exact position in the array of what I'm looking for
const arrayRetrievedChild = parent.children[0]; // no editor errors
parent.children.unshift(); // no editor errors
parent.children.push({ name: "Emily", age: 18 }); // no editor errors

Kind of a late response, but I looked through the typings and found the DocumentArray
import { Document, Embedded, Types } from 'mongoose';
interface IChild extends Embedded {
name: string;
}
interface IParent extends Document {
name: string;
children: Types.DocumentArray<IChild>;
}
Just wanted to put this here incase anyone else needs it.

Gross:
For now, I'm going with a very quick and dirty polyfill solution that doesn't actually answer my question:
declare module "mongoose" {
namespace Types {
class Collection<T> extends mongoose.Types.Array<T> {
public id: (_id: string) => (T | null);
}
}
}
then we declare IParent as such:
interface IParent {
name: string;
children: mongoose.Types.Collection<IChild>;
}
Because the function id() already exists and typescript just doesn't know about it, the code works and the polyfill lets it compile.
Even Grosser: Otherwise for an even quicker and dirtier solution, when you create the parent model instance simply typecast it to any and throw out all typescript checks:
const child = new ChildModel({name: "Lisa", age: 7});
const parent: any = new ParentModel({name: "Steve", children: [child]});
const idRetrievedChild = parent.children.id(childId); // works because declared any

Related

how to use dynamic field type in nestjs

I have a DTO structured like below, the field children I want to it to be number[] when client save the data.My database is MongoDB and I use Mongoose to manage my model.
//create-menu.dto.ts
export class CreateMenuDto {
...
#IsArray()
childrens: number[];
}
now I want it's type to be menu[] when the server response the client's query.So I define my Menu schema like below:
//menu.schema.ts
#Schema({ versionKey: false })
export class Menu {
...
#Prop({ type: Number, ref: 'Menu' })
childrens: Menu[];
}
menu.controller.ts:
#Put(':id')
update(#Param('id') id: string, #Body() updateMenuDto: UpdateMenuDto) {
return this.menuService.update(+id, updateMenuDto);
}
menu.service.ts:
update(id: number, updateMenuDto: UpdateMenuDto) {
const menu = this.menuModel.findOneAndUpdate({ _id: id }, updateMenuDto);
return menu;
}
when I use REST Client to test:
Put http://127.0.0.1:3000/api/menu/1
Content-Type: application/json
{
"childrens":[2]
}
the server throws 500 exception, so how can i implement dynamic field type? any idea is welcome, thanks!
The Menu Class has property childrens of type Menu[] while 'UpdateMenuDto' class has property childrens of type number[] so here :
const menu = this.menuModel.findOneAndUpdate({ _id: id }, updateMenuDto);
You got the error, because Nestjs cannot update the found menu document with the recieved updateMenuDto, I don't see any relation between an array of Menu and an array of number so I don't think the field childrens should exist in the UpdateMenuDto class neither in the PUT request body, but maybe you need these numbers to create a Menu array, in this case you can create a new object from updateMenuDto :
update(id: number, updateMenuDto: UpdateMenuDto) {
var newMenuDto: any = {...updateMenuDto};
newMenuDto.childrens = // use updateMenuDto.childrens to create newMenuDto.childrens of type Menu[]
const menu = this.menuModel.findOneAndUpdate({ _id: id }, newMenuDto);
return menu;
}

Change Typegoose collection name after initialize

I am using typegoose to create models. During creation of model, I found that it is possible to provide collection name. But once it is assigned, I am not able to find way to modify it.
export const MyModel: ModelType<MyModel> = new MyModel().setModelForClass(MyModel, {
existingMongoose: mongoose,
schemaOptions: {collection: 'my_collection_name'}
});
So in above MyModel, I want to change collection name where I am importing.
How can I change a collection name in model? Or am I only left with the option of creating this model where I want to use it?
Never mind. I just had to make function of exported object. So I changed it to below, so that I can pass collectionName where I am consuming this model.
const DocumentFieldBooleanValueModel = (collectionName: string) : ReturnModelType<typeof DocumentFieldBooleanValue, BeAnObject> => {
return getModelForClass(DocumentFieldBooleanValue, {
schemaOptions: { collection: collectionName },
});
};
export { DocumentFieldBooleanValueModel };
So now above exported model function I can use as below.
DocumentFieldBooleanValueModel('MyCustomCollectionName')
And it will give same typegoose model.

Transform JSON Response Field Name In Mongoose Model

My JSON response contains a field first_name but I want my Mongoose model to represent this field as firstName. Is this possible and if so then how?
You can create a new object with different property names from the one Mongoose returns. A nice way of doing this is to create a transform function. For example, let's say this is your schema:
{
firstName: { type: String, required: true },
lastName: { type: String, required: true }
}
Then you can use this function to create a new object with the desired property names:
function transformDocument (doc) {
return {
first_name: doc.firstName,
last_name: doc.lastName
}
}
Then, when you query the DB, you apply this function to the response:
Person.findOne({ firstName: 'John', lastName: 'Smith' })
.then(transformDocument)
Doug W has a good solution, but if you don't want to be using Promises and chaining .thens, then you can simply add options to the schema like this:
const mongoose = require ('mongoose'); // I am using v5.9.1 at the moment
const { Schema } = mongoose.Schema;
// Specify an options object
const options = {
toJSON: {
versionKey: false
}
// If you ever send the query result as an object,
// you may remove it from there, too, if you wish
// toObject: {
// versionKey: false
// }
};
// Attach the options object to the schema by
// passing it into Schema as the second argument
const mySchema = new Schema({
/** define your schema */
}, options);
This will still save __v to the document in the database. But it will not appear on the json/object when it is the result of a mongoose query.
Besides setting versionKey: false in the options, you may also specify a transform function:
/* ... */
// Specify an options object
const options = {
toJSON: {
// versionKey: false,
transform: function(doc, ret) {
// ret is the object that will be returned as the result
// (and then stringified before being sent)
delete ret.__v;
return ret;
}
}
};
/* ... */
I know this question is nearly two years old, but I needed an answer to this question, and google was not kind to me at the time. I figured it out, and now I'm hoping someone else will be looking for an answer here and find that they have options. Pun not originally intended.

Mongodb for HL7-FHIR

Learning FHIR and trying to implement with MEAN stack which uses MongoDb as database, I would like to seek your help on my question.
When I get the POST request for a new resource docment, I will insert it into MongoDB. Since the MongoDB will add the _id (object id) to the resources as a unique id. When I retrieve the document, it will have the extra field _id. I think it will make the resources not compliance any more since the _id is not defined in the resources.
May I know how to handle this issue? Will this extra _id matter in the FHIR resource?
Best regards,
Autorun
So, I'm also using MongoDB - along with mongoose - to implement FHIR in nodejs.
I've just added a field called id in the schema definition for mongoose like this
import mongoose from 'mongoose';
import shortid from 'shortid';
class resource extends mongoose.Schema {
constructor(schema) {
super();
this.add({
// just added this to make MongoDB use shortid
_id: { type: String, default: shortid.generate },
id: { type: {} },
id_: { type: {} },
implicitRules: { type: String },
implicitRules_: { type: {} },
language: { type: String },
language_: { type: {} },
...schema
});
}
}
export default resource;
and then _id field takes its value from the id when create/update a resource
my code for upserting a patient resource
upsert(root, params, context, ast) {
const projection = this.getProjection(ast);
if (!params.data.id) {
params.data.id = shortid.generate();
}
params.data.resourceType = 'Patient';
const upserted = model
.findByIdAndUpdate(params.data.id, params.data, {
new: true,
upsert: true,
select: projection
})
.exec();
if (!upserted) {
throw new Error('Error upserting');
}
return upserted;
}
yes, the _id will not be conformant. You can't change it to 'id'?
Perhaps you can take a look at the Spark server, which also uses a MongoDB to store the resources. In the Spark.Store.Mongo namespace you will see some helper methods to convert a Mongo BSONdocument to a FHIR resource.

MongoDB: output 'id' instead of '_id'

I am using mongoose (node), what is the best way to output id instead of _id?
Given you're using Mongoose, you can use 'virtuals', which are essentially fake fields that Mongoose creates. They're not stored in the DB, they just get populated at run time:
// Duplicate the ID field.
Schema.virtual('id').get(function(){
return this._id.toHexString();
});
// Ensure virtual fields are serialised.
Schema.set('toJSON', {
virtuals: true
});
Any time toJSON is called on the Model you create from this Schema, it will include an 'id' field that matches the _id field Mongo generates. Likewise you can set the behaviour for toObject in the same way.
See:
http://mongoosejs.com/docs/api.html
http://mongoosejs.com/docs/guide.html#toJSON
http://mongoosejs.com/docs/guide.html#toObject
You can abstract this into a BaseSchema all your models then extend/invoke to keep the logic in one place. I wrote the above while creating an Ember/Node/Mongoose app, since Ember really prefers to have an 'id' field to work with.
As of Mongoose v4.0 part of this functionality is supported out of the box. It's no longer required to manually add a virtual id field as explained by #Pascal Zajac.
Mongoose assigns each of your schemas an id virtual getter by default
which returns the documents _id field cast to a string, or in the case
of ObjectIds, its hexString. If you don't want an id getter added to
your schema, you may disable it passing this option at schema
construction time. Source
However, to export this field to JSON, it's still required to enable serialization of virtual fields:
Schema.set('toJSON', {
virtuals: true
});
I used this :
schema.set('toJSON', {
virtuals: true,
versionKey:false,
transform: function (doc, ret) { delete ret._id }
});
I think it would be great if they automatically suppress _id when virtuals is true.
I create a toClient() method on my models where I do this. It's also a good place to rename/remove other attributes you don't want to send to the client:
Schema.method('toClient', function() {
var obj = this.toObject();
//Rename fields
obj.id = obj._id;
delete obj._id;
return obj;
});
Here is an alternative version of the answer provided by #user3087827. If you find that schema.options.toJSON is undefined then you can use:
schema.set('toJSON', {
transform: function (doc, ret, options) {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
}
});
//Transform
Schema.options.toJSON.transform = function (doc, ret, options) {
// remove the _id of every document before returning the result
ret.id = ret._id;
delete ret._id;
delete ret.__v;
}
there is a "Schema.options.toObject.transform" property to do the reverse or you could just setup as a virtual id.
If you want to use id instead of _id globally then you can set toJSON config on mongoose object(starting from v5.3):
mongoose.set('toJSON', {
virtuals: true,
transform: (doc, converted) => {
delete converted._id;
}
});
Overwrite default method toJSON by new one:
schema.method('toJSON', function () {
const { __v, _id, ...object } = this.toObject();
object.id = _id;
return object;
});
There is also normalize-mongoose a simple package that removes _id and __v for you.
From something like this:
import mongoose from 'mongoose';
import normalize from 'normalize-mongoose';
const personSchema = mongoose.Schema({ name: String });
personSchema.plugin(normalize);
const Person = mongoose.model('Person', personSchema);
const someone = new Person({ name: 'Abraham' });
const result = someone.toJSON();
console.log(result);
So let's say you have something like this:
{
"_id": "5dff03d3218b91425b9d6fab",
"name": "Abraham",
"__v": 0
}
You will get this output:
{
"id": "5dff03d3218b91425b9d6fab",
"name": "Abraham"
}
I created an easy to use plugin for this purpose that I apply for all my projects and to all schema's globally. It converts _id to id and strips the __v parameter as well.
So it converts:
{
"_id": "400e8324a71d4410b9dc3980b5f8cdea",
"__v": 2,
"name": "Item A"
}
To a simpler and cleaner:
{
"id": "400e8324a71d4410b9dc3980b5f8cdea",
"name": "Item A"
}
Usage as a global plugin:
const mongoose = require('mongoose');
mongoose.plugin(require('meanie-mongoose-to-json'));
Or for a specific schema:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const MySchema = new Schema({});
MySchema.plugin(require('meanie-mongoose-to-json'));
Hope this helps someone.
You can also use the aggregate function when searching for items to return. $project will allow you to create fields, which you can do and assign it to _id.
<model>.aggregate([{$project: {_id: 0, id: '$_id'}], (err, res) => {
//
})
If you are using lodash to pick the elements you want, this will work for you.
UserSchema.virtual('id').get(function(){
return this._id.toHexString();
});
UserSchema.set('toObject', { virtuals: true })
UserSchema.methods.toJSON = function() {
return _.pick(
this.toObject(),
['id','email','firstName','lastName','username']
);
Override toJSONmethod for specific model schema.
https://mongoosejs.com/docs/api.html#schema_Schema-method
YourSchema.methods.toJSON = function () {
return {
id: this._id,
some_field: this.some_field,
created_at: this.createdAt
}
}
Create a base schema
import { Schema } from "mongoose";
export class BaseSchema extends Schema {
constructor(sche: any) {
super(sche);
this.set('toJSON', {
virtuals: true,
transform: (doc, converted) => {
delete converted._id;
}
});
}
}
Now in your mongoose model, use BaseSchema instead of Schema
import mongoose, { Document} from 'mongoose';
import { BaseSchema } from '../../helpers/mongoose';
const UserSchema = new BaseSchema({
name: String,
age: Number,
});
export interface IUser {
name: String,
age: Number,
}
interface IPlanModel extends IUser, Document { }
export const PlanDoc = mongoose.model<IPlanModel>('User', UserSchema);
Typescript implementation of #Pascal Zajac answer
There's another driver that does that http://alexeypetrushin.github.com/mongo-lite set convertId option to true. See "Defaults & Setting" section for more details.
Mongoose assigns each of your schemas an id virtual getter by default which returns the document's _id field cast to a string, or in the case of ObjectIds, its hexString.
https://mongoosejs.com/docs/guide.html
You can also use pre 'save' hook:
TouSchema.pre('save', function () {
if (this.isNew) {
this._doc.id = this._id;
}
}
JSON.parse(JSON.stringify(doc.toJSON()))