Reputation: 557
When using vanilla Mongoose, it's straight-forward to add methods to Mongoose schemas. It's well-addressed by the Mongoose documentation and several examples can be found.
But what about when you're using Mongoose within the context of a Nest app? I want my Mongoose schemas to be more "Nest-like", so I'm using the Nest wrapper for Mongoose (@nestjs/mongoose
). However, the documentation for @nestjs/mongoose
seems to be lacking. The closest thing I can find to any documentation is the guide for using MongoDB in a Nest app, and that only includes the most absolute basic use case for Mongoose.
To me, it looks like the way that Mongoose is used in the Nest world is very different from how vanilla Mongoose used. Maybe this is just a lack of TypeScript or Nest familiarity, but I can't seem to really navigate the differences, and the lack of examples isn't helping that.
I see a couple of answers for how to achieve this on StackOverflow, like:
How can I do something like this?
Schema
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type CatDocument = Cat & Document;
@Schema()
export class Cat {
@Prop()
name: string;
@Prop()
age: number;
@Prop()
breed: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
// ** Add methods here? **
Service
import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}
async findAll(): Promise<Cat[]> {
// Call our custom method here:
return this.catModel.doSomething();
}
}
Upvotes: 9
Views: 3607
Reputation: 1263
I like flyingpluto7's approach, but I had to adapt it to get rid of some issues with this
.
Specifically I needed:
this
this
@Schema()
export class Subscription {
@Prop(...)
tier: SubscriptionTier;
@Prop(...)
version: number;
}
interface SubscriptionStatics {
getCurrentVersion: (tier: SubscriptionTier) => Promise<number>
}
export type SubscriptionDocument = HydratedDocument<Subscription>;
export type SubscriptionModel = Model<Subscription> & SubscriptionStatics;
export const SubscriptionSchema = SchemaFactory.createForClass(Subscription);
/**
* I wasn't able to extract this into a separate file, no matter what I tried.
* Only by defining it next to the Schema was I able to use 'this' correctly.
*/
SubscriptionSchema.statics.getCurrentVersion = async function (tier: SubscriptionTier) {
const _version = await this.aggregate() // <- 'this' refers to the Model
.match({ tier }).sort('-version').limit(1).project({ version: 1, _id: 0 }).exec();
return _version.at(0)?.version ?? 0;
}
/**
* Inside the pre hook, I had to turn off typescript, since 'this' also only
* refers to the Model, not the SubscriptionModel.
*/
SubscriptionSchema.pre('validate', async function (next, options) {
if (!this.isNew) return next();
// @ts-ignore
const currentVersion: number = await this.model().getCurrentVersion(this.tier);
this.version = currentVersion + 1;
return next();
})
By doing it this way, I am now also able to call the static method in a service.
@Injectable()
export class SomeService {
constructor(
@InjectModel(Subscription.name)
private readonly subscriptionModel: SubscriptionModel
// ^ see definition above
) {}
async someMethod() {
return await this.subscriptionModel.getCurrentVersion(...);
}
}
There probably is a better way, I just don't know it yet. Here is a nestjs/mongoose issue on Github, which might help in finding an even better way.
Upvotes: 0
Reputation: 1099
This is how I use/implement statics and methods. Hope it helps.
user.schema.ts
import { modelToJSON, convertToPhotoURL, convertToUsername } from './user.statics';
import { testOnly } from './user.methods';
@Schema()
export class User {
...
}
interface UserStatics {
modelToJSON: (user: UserDocument) => User;
convertToPhotoURL: (value: string) => string;
convertToUsername: (value: string) => string;
}
interface UserMethods {
testOnly: () => void
}
export type UserDocument = User & Document & UserMethods;
export type UserModel = Model<UserDocument> & UserStatics;
export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.statics.modelToJSON = modelToJSON;
UserSchema.statics.convertToPhotoURL = convertToPhotoURL;
UserSchema.statics.convertToUsername = convertToUsername;
UserSchema.methods.testOnly = testOnly;
user.statics.ts
import { User, UserDocument } from "./user.schema";
export const modelToJSON = (user: UserDocument): User => {
return {
...
};
};
export const convertToPhotoURL = (value: string): string => {
return ...;
}
export const convertToUsername = (value: string): string => {
return ...;
}
user.methods.ts
import { User, UserModel } from "./user.schema";
export function testOnly() {
const model = this.model(User.name) as UserModel;
// do something with model
};
repository
async create(user: UserDto): Promise<User> {
const { modelToJSON, convertToPhotoURL, convertToUsername } = this.userModel;
const createdUser = new this.userModel(user);
createdUser.photoURL = convertToPhotoURL(user.displayName);
createdUser.username = convertToUsername(user.displayName);
return createdUser.save().then((model: UserDocument) => modelToJSON(model));
}
Upvotes: 0
Reputation: 45
Actually you are calling method on model, you have to call the method on created document, or returned document by model.
Method on schema.
UserSchema.methods.comparePassword = async function(candidatePassword: string) {
return await bcrypt.compare(candidatePassword, this.password);
};
Interface containing defined function.
export interface User {
comparePassword(candidatePassword: string): Promise<boolean>;
}
In my case I had already been using UserDocument which had User & Document. I add it when injecting model.
constructor(
@InjectModel(User.name) private readonly userModel: Model<UserDocument, UserFunction>
) {
}
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) { }
async signIn({ email, password }: SignInDto): Promise<LoginResponse> {
const user = await this.usersService.findByEmail(email); // Document found
if (!user) { throw new UnauthorizedException('Invalid Username or Password'); }
if (await user.comparePassword(password)) // <- Calling Schema method here {
const tokenPayload: JwtPayload = { userId: user.id };
const token = this.jwtService.sign(tokenPayload);
return ({ token, userId: user.id, status: LoginStatus.success });
} else {
throw new UnauthorizedException('Invalid Username or Password');
}
I had been calling the methods on userModel itself instead of calling it on it's document. Thanks to @Pantera answer I spotted the mistake.
Upvotes: 0
Reputation: 153
Here is what I managed to do:
export type UserDocument = User & Document;
@Schema()
export class User extends Document {
@Prop({ required: true, unique: true })
email!: string;
@Prop({ required: true })
passwordHash!: string;
toGraphql!: () => UserType;
}
export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.methods.toGraphql = function (this: User) {
const user = new UserType();
user.id = this._id;
user.email = this.email;
return user;
};
Just added
toGraphql!: () => UserType;
to class
Upvotes: 2