How can I use a TS type in a mongoose schema? I have the following:
...
type Associated = {
associatedId : string
imei : string
vehicleTypeId: string
}
interface IGroup extends Document {
...
associated: Associated
...
}
const Group = new Schema(
{
...
associated: {
type: Associated
},
...
},
{
collection: 'group'
}
)
...
But I getting an error: 'Associated' only refers to a type, but is being used as a value here.
I've never attempted it myself (although I very well might after this), but searching through the internet I found basically two options to achieve similar things:
User Louay Alakkad's answer found under this link Mongoose the Typescript way...?
export interface IUser extends mongoose.Document {
name: string;
somethingElse?: number;
};
export const UserSchema = new mongoose.Schema({
name: {type:String, required: true},
somethingElse: Number,
});
const User = mongoose.model<IUser>('User', UserSchema);
export default User;
A library closer to achieving what you described with 1.4k stars on GitHub and it looks like it's maintained with the last commit from a week ago https://github.com/typegoose/typegoose
I'm experiencing difficulties with Mongoose 5.10.0
Consider the following classes
Account:
export class Account {
_id: string;
email: string;
password: string;
phone: string;
address?: Address[] = [];
}
Address:
export class Address {
_id: string;
streetName: string;
zipCode: string;
city: string;
default: boolean | undefined;
}
And the appropriate DB models
Account:
import mongoose, { Schema, Document } from "mongoose";
import { Account } from "../classes/account.class";
import { addressSchema } from "./address.model";
export const accountSchema = new Schema({
_id: Schema.Types.ObjectId,
email: String,
phone: String,
password: String,
address: [addressSchema],
);
export type AccountModel = Document & Account;
export const Accounts = mongoose.model<AccountModel>("Account", accountSchema);
Address:
import mongoose, { Schema, Document } from "mongoose";
import { Address } from "../classes/address.class";
export const addressSchema: Schema = new Schema({
_id: Schema.Types.ObjectId,
streetName: String,
zipCode: String,
city: String,
default: Boolean,
});
export type AddressModel = Document & Address;
export const Addresses = mongoose.model<AddressModel>("Address", addressSchema);
Now when I get an account from the DB and try perform a pull against the address of the result, the pull() - method is not found at all and I get Property 'pull' does not exist on type 'Address[]'.
Is there something fundamentally wrong in the way the schema's are defined? Where am I going wrong?
In your code you're expecting MongoDB's pull operator to be a JavaScript method. You have two ways to achieve what you're trying to do:
You can either use .findOne() to retrieve the document, use JS code to modify the array (e.g. slice) and then run .save() to synchronize these changes with your db.
However if pulling an address is all you want to do then you can run an update operation directly on the database:
await Accounts.update({_id: idToFind}, {"$pull": { "address": { _id: addressId } } })
You'll end up having only one database call when using this 2nd approach
import { Types } from "mongoose";
import { AddressModel } from './Address';
const account = await Accounts.findOne({_id: idToFind});
const accountAddressArray = account.address as Types.DocumentArray<AddressModel>
accountAddress.pull(...)
according to #ZefirTheFear suggestion I tried somethings and it worked for me.
Actually I am using Nestjs. In my entity file I changed only one line:
It was like that.
#Prop({type: [UserAnimalsSchema]})
#Type(()=>UserAnimals)
userAnimals: UserAnimals[]
I changed it like that :
#Prop({type: [UserAnimalsSchema]})
#Type(()=>Types.DocumentArray<UserAnimals>)
userAnimals: Types.DocumentArray<UserAnimals>
I am in the process of implementing mongoose models with typescript as outlined in this article: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models and am not sure of how this translates when you are working with arrays of subdocuments. Let's say I have the following model and schema definitions:
interface IPet {
name: {type: mongoose.Types.String, required: true},
type: {type: mongoose.Types.String, required: true}
}
export = IPet
interface IUser {
email: string;
password: string;
displayName: string;
pets: mongoose.Types.DocumentArray<IPetModel>
};
export = IUser;
import mongoose = require("mongoose");
import IUser = require("../../shared/Users/IUser");
interface IUserModel extends IUser, mongoose.Document { }
import mongoose = require("mongoose");
import IPet = require("../../shared/Pets/IPet");
interface IPetModel extends IPet, Subdocument { }
code that would add a new pet to the user.pet subdocument:
addNewPet = (userId: string, newPet: IPet){
var _user = mongoose.model<IUserModel>("User", userSchema);
let userModel: IUserModel = await this._user.findById(userId);
let pet: IPetModel = userModel.pets.create(newPet);
let savedUser: IUser = await pet.save();
}
After reviewing the link, this seems to be the ideal approach necessary for handling subdocuments. However, this scenario seems to result in a CasterConstructor exception being thrown:
TypeError: Cannot read property 'casterConstructor' of undefined at Array.create.
Is it the right approach to dealing with Subdocuments when using mongoose models as outlined in the linked article above?
you can try this package https://www.npmjs.com/package/mongoose-ts-ua
#setSchema()
class User1 extends User {
#prop()
name?: string;
#setMethod
method1() {
console.log('method1, user1');
}
}
#setSchema()
class User2 extends User {
#prop({ required: true })
name?: string;
#prop()
child: User1;
}
export const User2Model = getModelForClass<User2, typeof User2>(User2);
usage
let u2 = new User2Model({ child: { name: 'u1' } });
I am currently trying to add a static method to my mongoose schema but I can't find the reason why it doesn't work this way.
My model:
import * as bcrypt from 'bcryptjs';
import { Document, Schema, Model, model } from 'mongoose';
import { IUser } from '../interfaces/IUser';
export interface IUserModel extends IUser, Document {
comparePassword(password: string): boolean;
}
export const userSchema: Schema = new Schema({
email: { type: String, index: { unique: true }, required: true },
name: { type: String, index: { unique: true }, required: true },
password: { type: String, required: true }
});
userSchema.method('comparePassword', function (password: string): boolean {
if (bcrypt.compareSync(password, this.password)) return true;
return false;
});
userSchema.static('hashPassword', (password: string): string => {
return bcrypt.hashSync(password);
});
export const User: Model<IUserModel> = model<IUserModel>('User', userSchema);
export default User;
IUser:
export interface IUser {
email: string;
name: string;
password: string;
}
If I now try to call User.hashPassword(password) I am getting the following error [ts] Property 'hashPassword' does not exist on type 'Model<IUserModel>'.
I know that I didn't define the method anywhere but I don't really know where I could put it as I can't just put a static method into an interface.
I hope you can help my find the error, thanks in advance!
I was having the same problem as you, and then finally managed to resolve it after reading the documentation in the TS mongoose typings (which I didn't know about before, and I'm not sure how long the docs have been around), specifically this section.
As for your case, you'll want to follow a similar pattern to what you currently have, although you'll need to change a few things in both files.
IUser file
Rename IUser to IUserDocument. This is to separate your schema from your instance methods.
Import Document from mongoose.
Extend the interface from Document.
Model file
Rename all instances of IUser to IUserDocument, including the module path if you rename the file.
Rename only the definition of IUserModel to IUser.
Change what IUser extends from, from IUserDocument, Document to IUserDocument.
Create a new interface called IUserModel which extends from Model<IUser>.
Declare your static methods in IUserModel.
Change the User constant type from Model<IUserModel> to IUserModel, as IUserModel now extends Model<IUser>.
Change the type argument on your model call from <IUserModel> to <IUser, IUserModel>.
Here's what your model file would look like with those changes:
import * as bcrypt from 'bcryptjs';
import { Document, Schema, Model, model } from 'mongoose';
import { IUserDocument } from '../interfaces/IUserDocument';
export interface IUser extends IUserDocument {
comparePassword(password: string): boolean;
}
export interface IUserModel extends Model<IUser> {
hashPassword(password: string): string;
}
export const userSchema: Schema = new Schema({
email: { type: String, index: { unique: true }, required: true },
name: { type: String, index: { unique: true }, required: true },
password: { type: String, required: true }
});
userSchema.method('comparePassword', function (password: string): boolean {
if (bcrypt.compareSync(password, this.password)) return true;
return false;
});
userSchema.static('hashPassword', (password: string): string => {
return bcrypt.hashSync(password);
});
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema);
export default User;
And your (newly renamed) ../interfaces/IUserDocument module would look like this:
import { Document } from 'mongoose';
export interface IUserDocument extends Document {
email: string;
name: string;
password: string;
}
I think you are having the same issue that I just struggled with. This issue is in your call. Several tutorials have you call the .comparePassword() method from the model like this.
User.comparePassword(candidate, cb...)
This doesn't work because the method is on the schema not on the model. The only way I was able to call the method was by finding this instance of the model using the standard mongoose/mongo query methods.
Here is relevant part of my passport middleware:
passport.use(
new LocalStrategy({
usernameField: 'email'
},
function (email: string, password: string, done: any) {
User.findOne({ email: email }, function (err: Error, user: IUserModel) {
if (err) throw err;
if (!user) return done(null, false, { msg: 'unknown User' });
user.schema.methods.comparePassword(password, user.password, function (error: Error, isMatch: boolean) {
if (error) throw error;
if (!isMatch) return done(null, false, { msg: 'Invalid password' });
else {
console.log('it was a match'); // lost my $HÏT when I saw it
return done(null, user);
}
})
})
})
);
So I used findOne({}) to get the document instance and then had to access the schema methods by digging into the schema properties on the document user.schema.methods.comparePassword
A couple of differences that I have noticed:
Mine is an instance method while yours is a static method. I'm confident that there is a similar method access strategy.
I found that I had to pass the hash to the comparePassword() function. perhaps this isn't necessary on statics, but I was unable to access this.password
For future readers:
Remember that we are dealing with two different Mongo/Mongoose concepts: a Model, and Documents.
Many Documents can be created from a single Model. The Model is the blueprint, the Document is the thing created according to the Model's instructions.
Each Document contains its own data. Each also carries their own individual instance methods which are tied to its own this and only operate on that one specific instance.
The Model can have 'static' methods which are not tied to a specific Document instance, but operate over the whole collection of Documents.
How this all relates to TypeScript:
Extend Document to define types for instance properties and .method functions.
Extend the Model (of a Document) to define types for .static functions.
The other answers here have decent code, so look at them and trace through the differences between how Documents are defined and how Models are defined.
And remember when you go to use these things in your code, the Model is used to create new Documents and to call static methods like User.findOne or your custom statics (like User.hashPassword is defined above).
And Documents are what you use to access the specific data from the object, or to call instance methods like this.save and custom instance methods like this.comparePassword defined above.
I cannot see your IUser interface however I suspect that you have not included the methods in there.
EG
export interface IUser {
email: string,
hash: string,
salt: string,
setPassword(password: string): void,
validPassword(password: string): boolean,
generateJwt(): string
}
typescript will then recognize your methods and stop complaining
So the one with 70 updates I also gave an upvote. But it is not a complete solution. He uses a trivial example based on the OP. However, more often than not when we use statics and methods in order to extend the functionality of the model, we want to reference the model itself. The problem with his solution is he using a callback function which means the value of this will not refer to the class context but rather a global.
The first step is to invoke the statics property rather than pass the property as an argument to the static function:
schema.statics.hashPassword
Now we cannot assign an arrow function to this member, for this inside the arrow function will still refer to the global object! We have to use function expression syntax in order to capture this in the context of the model:
schema.statics.hashPassword = async function(password: string): Promise<string> {
console.log('the number of users: ', await this.count({}));
...
}
Refer : https://mongoosejs.com/docs/typescript.html
Just create an interface before the schema which represents a document structure.
Add the interface type to the model.
Export the model.
Quoting below from mongoose docs:
import { Schema, model, connect } from 'mongoose';
// 1. Create an interface representing a document in MongoDB.
interface User {
name: string;
email: string;
avatar?: string;
}
// 2. Create a Schema corresponding to the document interface.
const schema = new Schema<User>({
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
});
// 3. Create a Model.
const UserModel = model<User>('User', schema);
If you add any method to the schema, add its definition in the interface as well.
I 'm trying to bind my Model with a mongoose schema using Typescript.
I have my IUser interface:
export interface IUser{
_id: string;
_email: string;
}
My User class:
export class User implements IUser{
_id: string;
_email: string;
}
My RepositoryBase:
export class RepositoryBase<T extends mongoose.Document> {
private _model: mongoose.Model<mongoose.Document>;
constructor(schemaModel: mongoose.Model<mongoose.Document>) {
this._model = schemaModel;
}
create(item: T): mongoose.Promise<mongoose.model<T>> {
return this._model.create(item);
}
}
And finally my UserRepository which extends RepositoryBase and implements an IUserRepository (actually empty):
export class UserRepository extends RepositoryBase<IUser> implements IUserRepository{
constructor(){
super(mongoose.model<IUser>("User",
new mongoose.Schema({
_id: String,
_email: String,
}))
)
}
}
Thr problem is that typescript compiler keeps saying :
Type 'IUser' does not satisfy the constraint 'Document'
And if I do:
export interface IUser extends mongoose.Document
That problem is solved but the compiler says:
Property 'increment' is missing in type 'User'
Really, i don't want my IUser to extend mongoose.Document, because neither IUser or User should know about how Repository work nor it's implementation.
I solved the issue by referencing this blog post.
The trick was to extends the Document interface from mongoose like so:
import { Model, Document } from 'mongoose';
interface User {
id: string;
email: string;
}
interface UserModel extends User, Document {}
Model<UserModel> // doesn't throw an error anymore