Sort the data by the populated field by typegoose in mongodb - 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.

Related

Typegoose is not saving array subdocuments properly in Nest.js app

I'm attempting to save an array of TaskOutcome subdocuments in my Task model, following the typegoose documentation:
import { prop } from '#typegoose/typegoose';
class TaskOutcome {
#prop({ required: true }) public inputId: string;
#prop() public label?: string;
#prop() public value?: string;
}
export class Task {
#prop() public _id: string;
...
#prop({ _id: false, type: () => [TaskOutcome] })
public outcomes: TaskOutcome[];
}
#Injectable()
export class TaskService {
constructor(
#InjectModel(Task) private readonly model: ReturnModelType<typeof Task>,
) {}
public async saveTask(data: TaskDTO) {
console.log(data.outcomes);
const options: QueryOptions = { upsert: true, new: true };
const result = await this.model.findByIdAndUpdate(data.id, data, options);
console.log(result.outcomes);
}
}
When I save a task document, all its props are saved as expected, except the outcomes array subdocuments. Instead, all I get is an array of empty objects. So even though the first console.log above shows me the correct input data, the second console.log gives me [{},{},{}] (confirmed when inspecting the database).
Anything obvious I'm doing wrong?
I'm using #nestjs/core#8.2.4, #typegoose/typegoose#9.3.1, nestjs-typegoose#7.1.38, and mongoose#6.1.2
Arrggghh. Ignore me. The code above is working perfectly fine.
Instead there seems to be a bug with the Nest.js ValidationPipe, which was very subtly mangling my incoming TaskDTO data - so my outcomes array looked fine at a glance in the console, but was actually an array of arrays, instead of an array of objects.
So typegoose was rightly ignoring it. If anyone stumbles on to this question, check the data you are saving really is exactly what it should be

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

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)

Typegoose Models and Many to Many Relationships

So I'm building a backend with NestJs and Typegoose, having the following models:
DEPARTMENT
#modelOptions({ schemaOptions: { collection: 'user_department', toJSON: { virtuals: true }, toObject: { virtuals: true }, id: false } })
export class Department {
#prop({ required: true })
_id: mongoose.Types.ObjectId;
#prop({ required: true })
name: string;
#prop({ ref: () => User, type: String })
public supervisors: Ref<User>[];
members: User[];
static paginate: PaginateMethod<Department>;
}
USER
#modelOptions({ schemaOptions: { collection: 'user' } })
export class User {
#prop({ required: true, type: String })
_id: string;
#prop({ required: true })
userName: string;
#prop({ required: true })
firstName: string;
#prop({ required: true })
lastName: string;
[...]
#prop({ ref: () => Department, default: [] })
memberOfDepartments?: Ref<Department>[];
static paginate: PaginateMethod<User>;
}
As you might guess, one user might be in many departments and one department can have many members(users). As the count of departments is more or less limited (compared with users), I decided to use one way embedding like described here: Two Way Embedding vs. One Way Embedding in MongoDB (Many-To-Many). That's the reason User holds the array "memberOfDepartments", but Department does not save a Member-array (as the #prop is missing).
The first question is, when I request the Department-object, how can I query members of it? The query must look for users where the department._id is in the array memberOfDepartments.
I tried multiple stuff here, like virtual populate: https://typegoose.github.io/typegoose/docs/api/virtuals/#virtual-populate like this on department.model:
#prop({
ref: () => User,
foreignField: 'memberOfDepartments',
localField: '_id', // compare this to the foreign document's value defined in "foreignField"
justOne: false
})
public members: Ref<User>[];
But it won't output that property. My guess is, that this only works for one-to-many on the one site... I also tried with set/get but I have trouble using the UserModel inside DepartmentModel.
Currently I'm "cheating" by doing this in the service:
async findDepartmentById(id: string): Promise<Department> {
const res = await this.departmentModel
.findById(id)
.populate({ path: 'supervisors', model: User })
.lean()
.exec();
res.members = await this.userModel.find({ memberOfDepartments: res._id })
.lean()
.exec()
if (!res) {
throw new HttpException(
'No Department with the id=' + id + ' found.',
HttpStatus.NOT_FOUND,
);
}
return res;
}
.. but I think this is not the proper solution to this, as my guess is it belongs in the model.
The second question is, how would I handle a delete of a department resulting in that i have to delete the references to that dep. in the user?
I know that there is documentation for mongodb and mongoose out there, but I just could not get my head arround how this would be done "the typegoose way", since the typegoose docs seem very limited to me. Any hints appreciated.
So, this was not easy to find out, hope this answer helps others. I still think there is the need to document more of the basic stuff - like deleting the references to an object when the object gets deleted. Like, anyone with references will need this, yet not in any documentation (typegoose, mongoose, mongodb) is given a complete example.
Answer 1:
#prop({
ref: () => User,
foreignField: 'memberOfDepartments',
localField: '_id', // compare this to the foreign document's value defined in "foreignField"
justOne: false
})
public members: Ref<User>[];
This is, as it is in the question, the correct way to define the virtual. But what I did wrong and I think is not so obvious: I had to call
.populate({ path: 'members', model: User })
explicitly as in
const res = await this.departmentModel
.findById(id)
.populate({ path: 'supervisors', model: User })
.populate({ path: 'members', model: User })
.lean()
.exec();
If you don't do this, you won't see the property members at all. I had problems with this because, if you do it on a reference field like supervisors, you get at least an array ob objectIds. But if you don't pupulate the virtuals, you get no members-field back at all.
Answer 2:
My research lead me to the conclusion that the best solution tho this is to use a pre-hook. Basically you can define a function, that gets called before (if you want after, use a post-hook) a specific operation gets executed. In my case, the operation is "delete", because I want to delete the references before i want to delete the document itself.
You can define a pre-hook in typegoose with this decorator, just put it in front of your model:
#pre<Department>('deleteOne', function (next) {
const depId = this.getFilter()["_id"];
getModelForClass(User).updateMany(
{ 'timeTrackingProfile.memberOfDepartments': depId },
{ $pull: { 'timeTrackingProfile.memberOfDepartments': depId } },
{ multi: true }
).exec();
next();
})
export class Department {
[...]
}
A lot of soultions found in my research used "remove", that gets called when you call f.e. departmentmodel.remove(). Do not use this, as remove() is deprecated. Use "deleteOne()" instead. With "const depId = this.getFilter()["_id"];" you are able to access the id of the document thats going to be deletet within the operation.

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.