Typegoose and Mongoose - Cast to ObjectId failed for value when saving nested list of schema types - mongodb

Getting this error:
Competition validation failed: results.0: Cast to ObjectId failed for value "{ name: 'David'}"
Here's the parent:
class Competition {
#prop()
compName: string
#prop({ ref: () => CompetitionParticipant})
results: Ref<CompetitionParticipant>[]
}
Here's the child:
class CompetitionParticipant {
#prop()
name: string
}
Here's how it's being called:
const CompetitionResults = getModelForClass(Competition)
await new CompetitionResults({compName: 'competition name', results: [{name: 'David'}]}).save()

this is because you try to save value { name: 'David' } as an reference, which is not supported (read here for more), you need to either provide and valid _id (in this case an ObjectId), or an instance of mongoose.Document
the easiest workaround is to manually loop over the array and save them individually (like in an bulk-insert call, or in an for loop over the array)

Related

Sort the data by the populated field by typegoose in mongodb

I have the three collections as following:
export class TopClass implements Base {
#prop()
name: string;
#prop({ ref: () => SecondClass})
second: SecondClass;
}
export class SecondClass implements Base {
#prop()
name: string;
#prop({ ref: () => ThirdClass})
third: ThirdClass;
}
export class ThirdClass implements Base {
#prop()
name: string;
#prop()
timestamp: number;
}
It is simple to retrieve the data in the "TopClass" by populate so that I can access the data by "topClass.second.third.timestamp" in the TypeScript. But I cannot sort the data for "TopClass" by the field "topClass.second.third.timestamp", how can I achieve it? For example, the result sort by "topClass.second.third.timestamp" should be:
// console.log(`${topClass.name} ${topClass.second.name} ${topClass.second.third.timestamp}`)
TopClassB, SecondClassB, 1648142800
TopClassA, SecondClassA, 1648142930
TopClassD, SecondClassD, 1648143055
TopClassC, SecondClassC, 1648143399
Not Working:
await this.topClassModel
.find()
.populate(...)
.sort({ 'second.third.timestamp': 1 });
Requirement:
sort the topClass by the populated field ('second.third.timestamp')
TopClass should be populated so that I can access each field from the results (ex. topClass.second.third.timestamp)
I also try to use the aggregate but it seems to be difficult to achieve the requirement 2. Anyone can help me?
TL;DR: You cant use mongoose populate in a query, use aggregation instead.
This Question with plain mongoose, and the Answer still applies to this because typegoose only handles class to schema / model translation not actual operations.

Missing Subdocument Methods in Mongoose with Typescript

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

Typescript & MongoDB type erasing when adding methods to the schema

I've been playing around with typescript and mongodb for the past few days and I wanted to add a custom method which I can execute on Document instances. Here is my setup:
import { Document, Schema, model, Model } from "mongoose";
import { AlbumSchema, AlbumDocument } from './album';
And here is my Document interface:
interface ArtistDocument extends Document {
name: string;
identifier: string;
albums: [AlbumDocument];
testFunction(): string
}
And my Schema:
const ArtistSchema = new Schema({
name: {type: String, required: true},
identifier: {type: String, required: true},
albums: {type: [AlbumSchema], required: true, default: []}
});
ArtistSchema.methods.testFunction = function(): string {
return "Hello World";
}
Note that I can just call testFunction(); on an instance of Artist just fine. So I know that methods are working.
Here is the issue though:
ArtistSchema.methods.testFunction = function(): string {
return "Albums:" + this.albums.length;
}
this.albums (which should be of type AlbumDocument[]) is somehow type any and therefore I can not use any array builtin functions nor can I filter and have AlbumDocument available to use its properties.
Is there anything that I am doing wrong? Is there a fix for it?
Try to instanciate albums like so :
albums:new Array<AlbumDocument>();

How do I use GraphQL with Mongoose and MongoDB without creating Mongoose models

Creating models in Mongoose is quite pointless since such models are already created with GraphQL and existing constructs (ie TypeScript interface).
How can we get GraphQL to use Mongoose's operations on models supplied from GraphQL without having to recreate models in Mongoose?
Also, it almost seems as if there should be a wrapper for GraphQL that just communicates with the database, avoiding having to write MyModel.findById etc
How does one do that?
Every example on the Internet that talks about GraphQL and Mongodb uses Mongoose.
You should look at GraphQL-to-MongoDB, or how I learned to stop worrying and love generated query APIs. It talks about a middleware package that leverages GraphQL's types to generate your GraphQL API and parses requests sent from clients into MongoDB queries. It more or less skips over Mongoose.
Disclaimer: this is my blog post.
The package generates GraphQL input types for your schema field args, and wraps around the resolve function to parse them into MongoDB queries.
Given a simple GraphQLType:
const PersonType = new GraphQLObjectType({
name: 'PersonType',
fields: () => ({
age: { type: GraphQLInt },
name: {
type: new GraphQLNonNull(new GraphQLObjectType({
name: 'NameType',
fields: () => ({
firstName: { type: GraphQLString },
lastName: { type: GraphQLString }
})
}))
}
})
});
For the most common use case, you'll build a field in the GraphQL schema with a getMongoDbQueryResolver and getGraphQLQueryArgs. The filter, projection, and options provided by the wrapper can be passed directly to the find function.
person: {
type: new GraphQLList(PersonType),
args: getGraphQLQueryArgs(PersonType),
resolve: getMongoDbQueryResolver(PersonType,
async (filter, projection, options, source, args, context) =>
await context.db.collection('person').find(filter, projection, options).toArray()
)
}
An example of a query you could send to such a field:
{
person (
filter: {
age: { GT: 18 },
name: {
firstName: { EQ: "John" }
}
},
sort: { age: DESC },
pagination: { limit: 50 }
) {
name {
lastName
}
age
}
}
There's also a wrapper and argument types generator for mutation fields.

Updating an array field in a mongodb collection

I am trying to update my collection which has an array field(initially blank) and for this I am trying this code
Industry.update({_id:industryId},
{$push:{categories: id:categoryId,
label:newCategory,
value:newCategory }}}});
No error is shown, but in my collection just empty documents({}) are created.
Note: I have both categoryId and newCategory, so no issues with that.
Thanks in advance.
This is the schema:
Industry = new Meteor.Collection("industry");
Industry.attachSchema(new SimpleSchema({
label:{
type:String
},
value:{
type:String
},
categories:{
type: [Object]
}
}));
I am not sure but maybe the error is occuring because you are not validating 'categories' in your schema. Try adding a 'blackbox:true' to your 'categories' so that it accepts any types of objects.
Industry.attachSchema(new SimpleSchema({
label: {
type: String
},
value: {
type: String
},
categories: {
type: [Object],
blackbox:true // allows all objects
}
}));
Once you've done that try adding values to it like this
var newObject = {
id: categoryId,
label: newCategory,
value: newCategory
}
Industry.update({
_id: industryId
}, {
$push: {
categories: newObject //newObject can be anything
}
});
This would allow you to add any kind of object into the categories field.
But you mentioned in a comment that categories is also another collection.
If you already have a SimpleSchema for categories then you could validate the categories field to only accept objects that match with the SimpleSchema for categories like this
Industry.attachSchema(new SimpleSchema({
label: {
type: String
},
value: {
type: String
},
categories: {
type: [categoriesSchema] // replace categoriesSchema by name of SimpleSchema for categories
}
}));
In this case only objects that match categoriesSchema will be allowed into categories field. Any other type would be filtered out. Also you wouldnt get any error on console for trying to insert other types.(which is what i think is happening when you try to insert now as no validation is specified)
EDIT : EXPLANATION OF ANSWER
In a SimpleSchema when you define an array of objects you have to validate it,ie, you have to tell it what objects it can accept and what it can't.
For example when you define it like
...
categories: {
type: [categoriesSchema] // Correct
}
it means that objects that are similar in structure to those in another SimpleSchema named categoriesSchema only can be inserted into it. According to your example any object you try to insert should be of this format
{
id: categoryId,
label: newCategory,
value: newCategory
}
Any object that isn't of this format will be rejected while insert. Thats why all objects you tried to insert where rejected when you tried initially with your schema structured like this
...
categories: {
type: [Object] // Not correct as there is no SimpleSchema named 'Object' to match with
}
Blackbox:true
Now, lets say you don't what your object to be filtered and want all objects to be inserted without validation.
Thats where setting "blackbox:true" comes in. If you define a field like this
...
categories: {
type: [Object], // Correct
blackbox:true
}
it means that categories can be any object and need not be validated with respect to some other SimpleSchema. So whatever you try to insert gets accepted.
If you run this query in mongo shell, it will produce a log like matched:1, updated:0. Please check what you will get . if matched is 0, it means that your input query is not having any matching documents.