NestJS Mongo references not saved properly - mongodb

I am working on a NestJS backend with Mongo but I am experiencing difficulties with the mongo references.
Let me explain the situation a bit more.
I have class called SystemInformation that contain fields like when was the object created or by who.
All the other schema of the application extend this class.
The field "createdBy" is a references to the User schema (that also extend SystemInformation).
When I am saving an object the payload contain the id of the user who created the record.
But when I look at the mongo database from Compass I see the field as a string, but never as a ref with the official format which look like :
{ "$ref" : <value>, "$id" : <value>, "$db" : <value> }
Here are the relevant part of the code is am using :
This is the system class and schema :
#ObjectType()
#Schema()
class SystemContent {
#Field()
#Prop({ required: true })
createdAt: number;
#Field()
#Prop({ default: 0 })
updatedAt: number;
#Field()
#Prop({ required: true, type: mongoose.Schema.Types.ObjectId, ref: 'User' })
createdBy: string;
#Field()
#Prop({ default: '' })
updatedBy: string;
}
#ObjectType()
#Schema()
export class SystemInformation {
#Field()
#Prop({ required: true })
system: SystemContent;
}
The User class as example of my extend implementation:
#Schema()
export class User extends SystemInformation {
id: string;
#Prop({ required: true, unique: true })
username: string;
#Prop({ required: true, unique: true })
email: string;
#Prop({ required: true })
hash: string;
#Prop({ default: false })
verified: boolean;
#Prop({ default: false })
enabled: boolean;
#Prop({ default: 0 })
bruteforce: number;
}
export const UserSchema = SchemaFactory.createForClass(User);
export type UserDocument = User & Document;
The payload and function that save to mongo is :
const payload = {
...
system: {
createdBy: '601c12060164023d59120cf43',
createdAt: 0,
},
};
const result = await new this.model(payload).save();
I wonder what I am doing wrong, could you guys please help me ?

but never as a ref with the official format which look like
mongoose dosnt use such an format, mongoose references work by saving the id directly as the type that it is on the target schema (objectid for objectid, string for string), and to look up which db an id is assigned, it probably uses the model on what connection & db it is created on
PS: typegoose references can be expressed with public property: Ref<TargetClass>
PPS: the official typegoose type to combine TargetClass and Document is called DocumentType<TargetClass>

Related

Nestjs Mongoose nested schema is not creating default values

Here is my code that I used in my entity
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import * as mongoose from 'mongoose';
#Schema({ _id: false })
export class Current {
#Prop({ default: '' })
operation: string;
#Prop({
default: '',
enum: [rentReportStatus.optin, rentReportStatus.optout, ''],
})
status: string;
#Prop({ default: Date.now() })
createdAt: Date;
}
const CurrentSchema = SchemaFactory.createForClass(Current);
#Schema({ collection: 'tests', timestamps: true })
export class Test extends BaseEntity {
#Prop({ type: CurrentSchema })
current: Current;
#Prop()
userId: mongoose.Schema.Types.ObjectId;
#Prop()
createdBy: mongoose.Schema.Types.ObjectId;
#Prop()
updatedBy: mongoose.Schema.Types.ObjectId;
}
export const RentReportingSchema = SchemaFactory.createForClass(RentReporting);
I tried many ways but current is not getting initialized with default values during save operation

Typegoose/ Mongoose nested discriminator is not accepting array

I am building a Node application using TypeScript and MongoDB as my application's database. I am using Typegoose to interact with MongoDB from Node JS application. Now, I am having a problem with using Nested Discriminator, https://typegoose.github.io/typegoose/docs/guides/advanced/nested-discriminators/ in my application.
I have a model called Workflow with the following code.
export class Workflow {
#prop({ required: true, type: Date, default: new Date() })
public createdAt!: Date;
#prop({ required: true, type: () => [WorkflowTask] })
public tasks!: WorkflowTask[];
}
export const WorkflowModel = getModelForClass(Workflow);
As you can see, a workflow can have many tasks (WorkflowTask). Below is the dummy code of my WorkflowTask model.
export class WorkflowTask {
#prop({ required: false, type: Schema.Types.String })
public title?: string;
#prop({ required: false, type: Schema.Types.String })
public description?: string;
#prop({ required: true, type: Schema.Types.String })
public type!: WorkflowTaskType;
#prop({
required: true,
type: FormContent,
discriminators: () => [
{
type: TextFormFieldContent,
value: FormWorkflowTaskContentType.TEXT_FORM_FIELD
},
{
type: NumberFormFieldContent,
value: FormWorkflowTaskContentType.NUMBER_FORM_FIELD
}
],
default: []
})
public formContents!: FormContent[];
}
As you can see, a WorkflowTask can have many FormContent where I put the discriminator in.
The following are some dummy code for my Workflow content classes.
export class FormContent {
#prop({ required: false, type: Schema.Types.String })
public label?: string;
#prop({ required: false, type: Schema.Types.String })
public message?: string;
#prop({ required: false, type: Schema.Types.String })
public description?: string;
#prop({ required: true, type: Schema.Types.String })
public type!: FormWorkflowTaskContentType;
}
export class TextFormFieldContent extends FormContent {
#prop({ required: false, type: Schema.Types.String })
public defaultValue?: string;
}
export class EmailFormFieldContent extends FormContent {
#prop({ required: false, type: Schema.Types.String })
public defaultValue?: string;
}
export class NumberFormFieldContent extends FormContent {
#prop({ required: false, type: Schema.Types.String })
public defaultValue?: string;
}
export class MultiSelectFieldContent extends FormContent {
#prop({ required: false, type: Schema.Types.Array, default: [] })
public defaultValue?: string[];
}
At the moment, I am only trying to create a workflow with empty tasks using the following code.
await WorkflowModel.create({
createdAt: new Date(),
tasks: []
});
I am getting the following error when I run the code.
ValidationError: Workflow validation failed: tasks: Cast to Embedded failed for value "[]" (type Array) at path "tasks" because of "ObjectExpectedError"
I am not even populating the tasks for the workflow. I followed the page correctly. What is wrong with my code and how can I fix it?
Looking at the typegoose documentation for the type option of the #prop decorator it seems like the type you pass should be the type of the array's items:
Example: Arrays (array item types can't be automatically inferred via Reflect)
class Dummy {
#prop({ type: String })
public hello: string[];
}
If that's the case the decorator may be defined as:
#prop({ required: true, type: () => WorkflowTask })
public tasks!: WorkflowTask[];
or
#prop({ required: true, type: WorkflowTask })
public tasks!: WorkflowTask[];
I dont see anything obviously wrong with your provided code, and i also locally tried to test your code which was working just fine.
Reproduction Repository / Branch: https://github.com/typegoose/typegoose-testing/tree/SO74916118
But there are some notes:
you can improve readability by replacing Schema.Types.String with String, typegoose & mongoose will automatically translate it to the proper Schema-Type
type: Schema.Types.Array is likely not what you want to set, this will effectively be of the Mixed type, in this case String would be appropiate
default: new Date() is likely not what you want, you probably want to change it to default: () => new Date(), the way it is currently defined evaluates default: new Date() at the time of loading the file once instead of everytime a document is created
some values were not provided and have been inferred in the reproduction code (not provided were FormWorkflowTaskContentType and WorkflowTaskType)
PS: another answer has pointed out that type: () => [WorkflowTask] is not supported, which is not true; the syntax of type: [Type] is supported since typegoose 7.4.0
Slight Note:
if you code fails, you may just be on old versions or you are running into Circular Dependencies

#Prop decorator for specific nested objects in array

I have an array of objects that returns in JSON, each object looks like that:
{
"reviewId": "f1a0ec26-9aca-424f-8b05-cff6aa3d2337",
"authorName": "Some name",
"comments": [
{
"userComment": {
"text": "\tAmazing!",
"lastModified": {
"seconds": "1659685904",
},
"starRating": 5,
"reviewerLanguage": "en",
}
},
{
"developerComment": {
"text": "Thank you.",
"lastModified": {
"seconds": "1659688852",
}
}
}
]
}
I'm trying to create a Schema for this specific object, but I have some issues and I cannot understand how to create it, this is what i've done so far:
import mongoose, { Document, Mongoose } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
export type ReviewDocument = Review & Document;
#Schema()
export class Review {
#Prop({ type: String, required: true })
reviewId: string;
#Prop({ type: String })
authorName: string;
#Prop({ type: mongoose.Schema.Types.Array })
comments: Comments[];
}
#Schema()
export class Comments {
#Prop({ type: mongoose.Schema.Types.ObjectId })
userComment: UserComment;
#Prop({ type: mongoose.Schema.Types.ObjectId })
developerComment: DeveloperComment;
}
#Schema()
export class UserComment {
#Prop({ type: String })
text: string;
#Prop({ type: String })
originalText: string;
#Prop({ type: String })
lastModified: string;
#Prop({ type: Number })
starRating: number;
#Prop({ type: String })
reviewerLanguage: string;
}
#Schema()
export class DeveloperComment {
#Prop({ type: String })
text: string;
#Prop({ type: String })
lastModified: string;
}
export const ReviewSchema = SchemaFactory.createForClass(Review);
It gives me an error:
/.../rtr-backend/src/schemas/reviews.schema.ts:21
userComment: UserComment;
^
ReferenceError: Cannot access 'UserComment' before initialization
at Object.<anonymous> (/.../rtr-backend/src/schemas/reviews.schema.ts:21:18)
at Module._compile (node:internal/modules/cjs/loader:1120:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
at Module.load (node:internal/modules/cjs/loader:998:32)
at Function.Module._load (node:internal/modules/cjs/loader:839:12)
at Module.require (node:internal/modules/cjs/loader:1022:19)
at require (node:internal/modules/cjs/helpers:102:18)
at Object.<anonymous> (/.../rtr-backend/src/reviews/reviews.module.ts:6:1)
at Module._compile (node:internal/modules/cjs/loader:1120:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
What is best practice?
There are a few things to notice when you define a mongoose schema:
The schema types of primitive properties (e.g. string, number) are automatically inferred, so you don't need to explicitly specify { type: String} in the decorator.
In order to preserve the nested schema validation, each object has to have its own schema or be defined using the raw schema definition.
For each nested schema please mind the default properties created by mongoose, such as timestamps, _id, __v, and so on. You could manipulate them by passing options object in the #Schema() decorator.
Here is the schema definition for your use case:
import { Prop, raw, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document, Types } from 'mongoose';
#Schema({ _id: false })
class UserComment {
#Prop({ required: true })
text: string;
#Prop(raw({ seconds: { type: Number } }))
lastModified: Record<string, number>;
#Prop({ required: true })
starRating: number;
#Prop({ required: true })
reviewerLanguage: string;
}
const UserCommentSchema = SchemaFactory.createForClass(UserComment);
#Schema({ _id: false })
class DeveloperComment {
#Prop({ required: true })
text: string;
#Prop(raw({ seconds: { type: Number } }))
lastModified: Record<string, number>;
}
const DeveloperCommentSchema = SchemaFactory.createForClass(DeveloperComment);
#Schema({ _id: false })
class Comment {
#Prop({ type: UserCommentSchema })
userComment?: UserComment;
#Prop({ type: DeveloperCommentSchema })
developerComment?: DeveloperComment;
}
const CommentSchema = SchemaFactory.createForClass(Comment);
#Schema({ timestamps: true, versionKey: false })
export class Review {
_id: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
#Prop({ unique: true, required: true })
reviewId: string;
#Prop({ required: true })
authorName: string;
#Prop({ type: [CommentSchema], default: [] })
comments: Comment[];
}
export type ReviewDocument = Review & Document;
export const ReviewSchema = SchemaFactory.createForClass(Review);
PS: In this documentation page you could find plenty of use cases: https://docs.nestjs.com/techniques/mongodb
I think you need to first define the schemas in the order of their dependencies, like this:
import mongoose, { Document, Mongoose } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
export type ReviewDocument = Review & Document;
#Schema()
export class UserComment {
#Prop({ type: String })
text: string;
#Prop({ type: String })
originalText: string;
#Prop({ type: String })
lastModified: string;
#Prop({ type: Number })
starRating: number;
#Prop({ type: String })
reviewerLanguage: string;
}
#Schema()
export class DeveloperComment {
#Prop({ type: String })
text: string;
#Prop({ type: String })
lastModified: string;
}
#Schema()
export class Comments {
#Prop({ type: mongoose.Schema.Types.ObjectId })
userComment: UserComment;
#Prop({ type: mongoose.Schema.Types.ObjectId })
developerComment: DeveloperComment;
}
#Schema()
export class Review {
#Prop({ type: String, required: true })
reviewId: string;
#Prop({ type: String })
authorName: string;
#Prop({ type: mongoose.Schema.Types.Array })
comments: Comments[];
}
export const ReviewSchema = SchemaFactory.createForClass(Review);

nestjs mongoose Type 'string' is not assignable to type 'Condition<LeanDocument<User>>

Im using nestjs with mongoose. I try to do a simple find query
this.challengesModel.find({ createdBy: userId })
where this.challengesModel is injected like this
private readonly challengesModel: Model<Challenge>
but it says
Type 'string' is not assignable to type 'Condition<LeanDocument<User>>'
createdBy is considered as a User object whereas I am giving it a string(userId)
How can I still keep the createdBy field as User by search only by the id?
This is my schema
#Schema()
export class Challenge extends Document {
#Prop({ type: mongoose.Schema.Types.ObjectId, ref: "User" })
createdBy: User;
#Prop()
description: string;
#Prop({ default: new Date() })
creationTime: Date;
#Prop()
video: string;
#Prop({ default: [] })
likes: string[];
#Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }] })
selectedFriends: User[];
#Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: "Reply" }] })
replies: Reply[];
}
The createdBy is saved as a unique id of the user (foreign key to the users collection)
I am trying to perform a query to find challenges that were created by a user with specific id.
If I change createdBy to be a string(id) it works, but then I don't get all the user properties, also the nest documentation suggests to create it like I did.
What should I change in order to be able to do this find without any compliation errors?
Try
this.challengesModel.find({ createdBy: userId as any })
This is caused because createdBy isn't a string type.

How to set up Unique Compound Index in NestJS (MongoDB)

Hey i have a schema for user with an unique email:
#Schema()
export class User {
#Prop()
firstName!: string;
#Prop()
lastName!: string;
#Prop({
unique: true,
})
email!: string;
#Prop({ nullable: true })
password?: string;
}
But now i want to extend this. I wanna have Groups with their own users. I would create a collection of groups and add their id to the users like:
#Schema()
export class User {
#Prop()
groupId: string;
#Prop()
firstName!: string;
...
}
For each group the email should be unique. That means in the collection of users there could be duplicate emails but they should be unique by group which is named Unique Compound Index i guess.
How do i set this up in NestJS?
Looks like its not possible to achieve it solely by use of the #nestjs/mongoose decorators, however its possible to declare index by use of SchemaFactory
#Schema()
export class User {
#Prop()
groupId: string;
#Prop()
firstName!: string;
...
}
export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.index({ groupId: 1, firstName: 1 }, { unique: true });
Then you must do either create migration to create this index or enable auto-index feature
#Schema({
autoIndex: true, // <--
})
export class User {
#Prop()
groupId: string;
#Prop()
firstName!: string;
...
}