Creating mongoose models with typescript - subdocuments - node.js

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' } });

Related

How to extend Mongoose Schema in Typescript

I'm making 3 schemas (article, comment, user) and models that share some fields.
FYI, I'm working with mongoose and typescript.
mongoose v6.1.4
nodejs v16.13.1
typescript v4.4.3
interface of each 3 schema shares a common interface UserContent, and they looks like this:
interface IUserContent {
slug: string;
should_show: 'always' | 'never' | 'by_date';
show_date_from: Date | null;
show_date_to: Date | null;
published_date: Date | null;
}
interface IArticle extends IUserContent {
title: string;
content: string;
user_id: number;
}
interface IComment extends IUserContent {
content: string;
user_id: number;
}
interface IUser extends IUserContent {
name: string;
description: string;
}
And I'm trying to make an function which creates Mongoose Schema with shared fields:
import { Schema, SchemaDefinition } from 'mongoose'
const createUserContentSchema = <T extends object>(fields: SchemaDefinition<T>) => {
const schema = new Schema<IUserContent & T>({
// theese fields are shared fields
slug: { type: String },
should_show: { type: String, enum: ['always', 'never', 'by_date'] },
show_date_from: { type: Date },
show_date_to: { type: Date },
published_date: { type: Date },
// this is not-shared fields
...fields,
})
return schema
}
I was expected that this function will create schema with shared fields and non-shared fields combined together. (like code below)
const UserSchema = createUserContentSchema<IUser>({
name: {type: String},
description: {type: String},
});
However, It throws Type Error on the object parameter in new Schema which is inside createUserContentSchema function. (nevertheless compiled javascript code works well as expected)
Type '{ slug: { type: StringConstructor; }; should_show: { type: StringConstructor; enum: string[]; }; show_date_from: { type: DateConstructor; }; show_date_to: { ...; }; published_date: { ...; }; } & SchemaDefinition' is not assignable to type 'SchemaDefinition<SchemaDefinitionType<IUserContent & T>>'.ts(2345)
I removed generic from createUserContentSchema function and directly replaced T to IUser and it turns out to be nice without error. So, I'm assuring that I made mistake in typing generic. but can't figure out what did I make wrong exactly.
I want to fix my code to not make this Type Error.
PS
I found out that my error is reproduced only in mongoose#v6 (not v5)
I read the breaking changes in update note but can't figure why this error is being produced in v6.
Mongoose Discriminator sounds like a feature you need.
https://mongoosejs.com/docs/api.html#model_Model.discriminator
function BaseSchema() {
Schema.apply(this, arguments);
this.add({
name: String,
createdAt: Date
});
}
util.inherits(BaseSchema, Schema);
const PersonSchema = new BaseSchema();
const BossSchema = new BaseSchema({ department: String });
const Person = mongoose.model('Person', PersonSchema);
const Boss = Person.discriminator('Boss', BossSchema);
new Boss().__t; // "Boss". `__t` is the default `discriminatorKey`
const employeeSchema = new Schema({ boss: ObjectId });
const Employee = Person.discriminator('Employee', employeeSchema, 'staff');
new Employee().__t; // "staff" because of 3rd argument above

Mongoose: Property 'pull' does not exist on type 'Address[]'

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>

Sharing typescript interfaces between client and server mongoose model

I'm trying to share some base interfaces between the client code and the server code. I'm having problems when using the interfaces to create data models in mongoose.
The problem I have is how to access the document._id property in the client. I can't add _id to the User interface without causing compilation errors and I can't access _id without declaring it.
My project layout:
/src
-/client
--/user.service.ts
-/server
--/models
---/user.model.ts
-/common
--/common.d.ts
user.service.ts
import { User } from 'common'
deleteUser(user: User): Promise<User> {
return this.http.delete( 'http://someurl/api/users' + user._id )
.toPromise()
.then( resp => resp.json().data as User )
.catch( err => this.errorHandler(err) );
}
user.model.ts
import { model, Schema, Document } from 'mongoose';
import { User } from 'common';
let UserSchema = new Schema {
firstName: String,
lastName: String,
email: String
}
export interface UserDocument extends User, Document { }
export var UserModel:Model<UserDocument> = model<UserDocument>('Users', UserSchema);
common.d.ts
declare module 'common' {
export interface User {
firstName: string;
lastName: string;
email: string;
}
}
Thanks for the help
You can declare _id as optional:
export interface User {
_id?: string;
firstName: string;
lastName: string;
email: string;
}
Or you can have another interface for a user with id:
export interface PersistedUser extends User {
_id: string;
}
And cast to it whenever needed.

Mongoose typing issue with typescript

I am building an app using mongoose and typescript. Here is a simple model I have made:
import * as callbackMongoose from 'mongoose';
var mongoose = callbackMongoose;
mongoose.Promise = global.Promise;
const Schema = mongoose.Schema;
var userSchema = new Schema({
username: String,
email: String,
hash: String
});
export default mongoose.model('User', userSchema);
It works well but I need to cast each document to any before accessing properties. I read a guide that said I could do this:
interface IUser extends mongoose.Document {
username: String;
email: String;
hash: String;
}
export default mongoose.model<IUser>('User', userSchema);
My problem is that the type mongoose doesn't seem to have the property Document. It also doesn't have the property ObjectId. When I cast mongoose to any and use these members it works just fine. It seems to be a typing issue.
I installed the mongoose typing like so:
npm install #types/mongoose --save
The typings do work for Schema and they are good for all of the other libraries I use. Is something wrong with these type definitions? Am I doing something wrong?
For TypeScript#2.0
I think you may use
npm install #types/mongoose --save
instead of:
npm install #typings/mongoose --save
This is full example:
Database.ts
import mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.connect('mongodb://admin:123456#ds149437.mlab.com:49437/samples');
export { mongoose };
UserData.ts
import { mongoose } from './../../Services/Database';
export interface UserData {
is_temporary: boolean;
is_verified: boolean;
status: boolean;
username: string;
}
export interface IUserData extends UserData, mongoose.Document, mongoose.PassportLocalDocument { };
UserModel.ts
import { IUserData } from './UserData';
import { mongoose } from './../../Services/Database';
import * as passportLocalMongoose from 'passport-local-mongoose';
import Schema = mongoose.Schema;
const UserSchema = new Schema({
username: { type: String, required: true },
password: String,
status: { type: Boolean, required: true },
is_verified: { type: Boolean, required: true },
is_temporary: { type: Boolean, required: true }
});
UserSchema.plugin(passportLocalMongoose);
var UserModel;
try {
// Throws an error if 'Name' hasn't been registered
UserModel = mongoose.model('User')
} catch (e) {
UserModel = mongoose.model<IUserData>('User', UserSchema);
}
export = UserModel;
I also full project example using typescript, node.js, mongoose & passport.js right here: https://github.com/thanhtruong0315/typescript-express-passportjs
Good luck.

Mongoose + Typescript -> Exporting model interface

I want to export only my model's interfaces instead of the Document so that nobody can modify my model if it's not inside it's own class methods. I have defined the interface and the schema like this:
IUser:
interface IUser {
_id: string;
name: string;
email: string;
created_at: number;
updated_at: number;
last_login: number;
}
And the Schema:
let userSchema: Mongoose.Schema = new Mongoose.Schema({
'name': String,
'email': String,
'created_at': {'type': Date, 'default': Date.now},
'updated_at': {'type': Date, 'default': Date.now},
'last_login': {'type': Number, 'default': 0},
});
interface UserDocument extends IUser, Mongoose.Document {}
And then the model
// Model
let Users: Mongoose.Model<UserDocument> = Mongoose.model<UserDocument>('User', userSchema);
So i just export the IUser and a class User that basically has all the methods to update my model.
The problem is that typescript complains if i add the _id to my interface, but i actually need it, otherwise i will need to pass the UserDocument and that's what i didn't wanted to do. The error typescript gives me is:
error TS2320: Interface 'UserDocument' cannot simultaneously extend types 'IUser' and 'Document'.
Named property '_id' of types 'IUser' and 'Document' are not identical.
Any ideas how i can add the _id property to my interface?
Thanks!
Try:
interface UserDocument extends IUser, Mongoose.Document {
_id: string;
}
It will resolve the conflict between IUser._id (string) vs Mongoose.Document._id (any).
Update:
As pointed out in comments, currently it gives a incompatible override for member from "Document", so another workaround must be used. Intersection types is a solution that can be used. That said, the following can be done:
type UserDocument = IUser & Mongoose.Document;
Alternatively, if you do not want UserDocument anymore:
// Model
let Users = Mongoose.model<IUser & Mongoose.Document>('User', userSchema);
It is worth noting that there is a side effect in this solution. The conflicting properties will have the types intersected, so IUser._id (string) & Mongoose.Document._id (any) results in UserDocument._id (any), for example.
I just had this exact issue, where I wanted to keep the User interface properties as separate from Mongoose as possible. I managed to solve the problem using the Omit utility type.
Here is your original code using that type:
import { Document, Model, ObjectId } from 'mongoose';
export interface IUser {
_id: ObjectId;
name: string;
email: string;
created_at: number;
updated_at: number;
last_login: number;
}
export interface IUserDocument extends Omit<IUser, '_id'>, Document {}
export interface IUserModel extends Model<IUserDocument> {}
try this:
const UserSchema: Schema = new Schema(
{
..
}
);
type UserDoc = IUser & Document;
export interface UserDocument extends UserDoc {}
// For model
export interface UserModel extends Model<UserDocument> {}
export default model<UserDocument>("User", UserSchema);

Resources