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
Related
I am going to provide some code since it might be hard to follow what is going on. Essentially, my Express API doesn't return any associations even though my database thinks there are.
High level quickly: I am trying to get an assocation between two models Users and Donations, One-To-Many.
Here is my User Model (simplified):
import { Sequelize, DataTypes, Model, BuildOptions, Optional, Association, } from "sequelize";
// These are all the attributes in the User model
export interface UserAttributes {
id: number;
firstName: string;
lastName: string;
createdAt?: Date;
updatedAt?: Date;
}
// Optional properties, sequelize has the ability to fill these in itself
interface UserCreationAttributes
extends Optional<
UserAttributes,
"id"
> {}
// Creates the final type interface for the UserModel
export interface UserModel
extends Model<UserAttributes, UserCreationAttributes>,
UserAttributes {}
// Creates the User class as an extended type of a general model and UserModel
export class User extends Model<UserModel, UserAttributes> {
// Public properties
public id!: number;
public firstName!: string;
public lastName!: string;
// Timestamps
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
// Associations
public static associations: {
donations: Association<User, Donation>;
};
}
// Creates a static type used for our factory function
export type UserStatic = typeof Model & {
new (values?: object, options?: BuildOptions): UserModel;
};
// This function is responsible for generating the "User" model
export function userFactory(sequelize: Sequelize): UserStatic {
return <UserStatic>sequelize.define(
"users",
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
firstName: {
type: new DataTypes.STRING(128),
allowNull: false,
validate: { notEmpty: true },
},
lastName: {
type: DataTypes.STRING,
allowNull: false,
validate: { notEmpty: true },
},
});
}
Donation Model is set up largely the same way, I don't want to flood you with code so I will just go to the part where I do the associations.
// Import the factory functions and sequelize instance, called "db" here
import { donationFactory } from "./Donation";
import { userFactory } from "./User";
import { db } from "../db";
// The Factory functions expect a Sequelize instance as a parameter
const User = userFactory(db);
const Donation = donationFactory(db);
const DonationsRecipients = donationsRecipientsFactory(db);
// Associations
Donation.belongsTo(User);
User.hasMany(Donation);
// Export model
export const model = {
Donation,
DonationsRecipients,
User,
};
Okay, so you can see there I am declaring a very simple One-to-Many relationship between the Donation and User models. Below, I will show you my code for my express route.
// Get all donors
userGetRoutes.get("/donors", async (req, res, next) => {
try {
const users = await User.findAll({
where: { isDonor: true },
include: [{ model: Donation }],
});
res.status(200).send(users);
} catch (error) {
next(error);
}
});
But I only get this!!! The included donations are not there... I cannot access the donation objects
And what is even weirder is that Postico thinks there IS an association. You can tell because when you click the little table in my Donation model, the associated user pops up (Monica Moneybags).
So... what gives??
EDIT:
Okay so I've made some progress. I got user instances to be able to call "getDonations()" and other magic methods, which do in fact return the donations.
To do this, I adjusted my UserModel and User classes to look like this:
import {
Sequelize,
DataTypes,
Model,
BuildOptions,
Optional,
Association,
HasManyGetAssociationsMixin,
HasManyAddAssociationMixin,
HasManyHasAssociationMixin,
HasManyCountAssociationsMixin,
HasManyCreateAssociationMixin,
} from "sequelize";
/ Creates the final type interface for the UserModel
export interface UserModel
extends Model<UserAttributes, UserCreationAttributes>,
UserAttributes {
donations: DonationModel[];
getDonations(): HasManyGetAssociationsMixin<Donation>; // Note the null assertions!
addDonation(): HasManyAddAssociationMixin<Donation, number>;
hasDonation(): HasManyHasAssociationMixin<Donation, number>;
countDonations(: HasManyCountAssociationsMixin;
createDonation(): HasManyCreateAssociationMixin<Donation>;
}
// Creates the Donation classs as an extended type of a general model and UserModel
export class User extends Model<UserModel, UserAttributes> {
// Public properties
public id!: number;
public firstName!: string;
public lastName!: string;
// Timestamps
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
// Associations
//
// Since TS cannot determine model association at compile time
// we have to declare them here purely virtually
// these will not exist until `Model.init` was called.
public getDonations!: HasManyGetAssociationsMixin<Donation>; // Note the null assertions!
public addDonation!: HasManyAddAssociationMixin<Donation, number>;
public hasDonation!: HasManyHasAssociationMixin<Donation, number>;
public countDonations!: HasManyCountAssociationsMixin;
public createDonation!: HasManyCreateAssociationMixin<Donation>;
public readonly donations?: Donation[]; // Note this is optional since it's only populated when explicitly requested in code
public static associations: {
donations: Association<User, Donation>;
};
}
Still though, inclusions do not work on called like:
const users = await User.findOne({
where: { isDonor: true },
include: [{ model: Donation }],
});
It just returns an empty array in the "donations" property :/
I create POST endpoint to create a new entity.
I also created schema for mongoose with field userId (to connect this entity to specified user) and DTO which I use on my POST method.
#UseGuards(JwtAuthGuard)
#Post("/")
createAction(#Request() req, #Body() createActionDto: CreateActionDto) {
return this.actionService.createAction(req?.user?.userId, createActionDto);
}
DTO:
import { IsString, IsNumber, IsUrl } from 'class-validator';
export class CreateActionDto {
userId: string;
#IsString()
name: string;
#IsNumber()
timeStart: number;
}
Schema:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
#Schema()
export class Action extends Document {
#Prop()
userId: string;
#Prop()
name: string;
#Prop()
timeStart: number;
}
export const ActionSchema = SchemaFactory.createForClass(Action)
In the req property I have userId. What is the best way to create an entity and attach userId extracted from token?
Should I pass req to the service, and in the service set userId property on DTO like this?:
#Injectable()
export class ActionService {
constructor(
#InjectModel(Action.name) private actionModel: Model<Action>,
) { }
async createAction(req: string, createActionDto: CreateActionDto) {
createActionDto.userId = req.user.userId
// ... save to mongoose createActionDto
}
}
Is it a correct solution or there is another, a better way to deal with it?
Personally I would set the userId in the controller in order to not having to pass it around:
#UseGuards(JwtAuthGuard)
#Post("/")
createAction(#Request() req, #Body() createActionDto: CreateActionDto) {
createActionDto.userId = req?.user?.userId;
return this.actionService.createAction(createActionDto);
}
If you have many different controllers and DTOs that require the userId you could also define an Interceptor and do it there in order to reduce duplication:
#Injectable()
export class SetUserIdInterceptor implements NestInterceptor {
public intercept(_context: ExecutionContext, $next: CallHandler): Observable<any> {
const request: any = _context.switchToHttp().getRequest(); //instead of any you could also define a super-class for all DTOs that require the `userId`-property
request.body?.userId = req?.user?.userId;
return $next;
}
}
You can then use this interceptor on your route as follows:
#UseGuards(JwtAuthGuard)
#Post("/")
#UseInterceptors(SetUserIdInterceptor)
createAction(#Body() createActionDto: CreateActionDto) {
return this.actionService.createAction(createActionDto)
}
I am trying to create simple appliaction with Nest.js, GraphQL and MongoDB. I wnated to use TypeORM and TypeGraphql to generate my schema and make a connection with localhost databasebut but i can not run my server with nest start becouse I am getting this error:
UnhandledPromiseRejectionWarning: Error: Cannot determine GraphQL output type for getArticles
I have no idea why i am getting this error. My class ArticleEntity does't has any not primary types, so there should not be any problem. I tried to remove () => ID from #Field() decorator of filed _id of ArticleEntity class but it didn't helped
ArticleResolver
#Resolver(() => ArticleEntity)
export class ArticlesResolver {
constructor(
private readonly articlesService: ArticlesService) {}
#Query(() => String)
async hello(): Promise<string> {
return 'Hello world';
}
#Query(() => [ArticleEntity])
async getArticles(): Promise<ArticleEntity[]> {
return await this.articlesService.findAll();
}
}
ArticleService
#Injectable()
export class ArticlesService {
constructor(
#InjectRepository(ArticleEntity)
private readonly articleRepository: MongoRepository<ArticleEntity>,
) {}
async findAll(): Promise<ArticleEntity[]> {
return await this.articleRepository.find();
}
}
ArticleEntity
#Entity()
export class ArticleEntity {
#Field(() => ID)
#ObjectIdColumn()
_id: string;
#Field()
#Column()
title: string;
#Field()
#Column()
description: string;
}
ArticleDTO
#InputType()
export class CreateArticleDTO {
#Field()
readonly title: string;
#Field()
readonly description: string;
}
If you need anything else comment
ArticleEntity should be decorated with the #ObjectType decorator as shown in the docs https://typegraphql.com/docs/types-and-fields.html.
#Entity()
#ObjectType()
export class ArticleEntity {
...
}
For anyone who gets this error and uses enums, you may be missing a call to registerEnumType.
In my case, I was using the #ObjectType decorator, but I was importing from type-graphql. I imported from #nestjs/graphql instead, and the problem was resolved.
import { ObjectType } from '#nestjs/graphql';
See here for a related discussion on GitHub.
I was using MongoDB and I had my Query return the schema instead of the model class.
Changing #Query((returns) => UserSchema) to #Query((returns) => User) fixed the issue for me.
user.schema.ts
#ObjectType()
#Schema({ versionKey: `version` })
export class User {
#Field()
_id: string
#Prop({ required: true })
#Field()
email: string
#Prop({ required: true })
password: string
}
export const UserSchema = SchemaFactory.createForClass(User)
user.resolver.ts
#Query((returns) => User)
async user(): Promise<UserDocument> {
const newUser = new this.userModel({
id: ``,
email: `test#test.com`,
password: `abcdefg`,
})
return await newUser.save()
}
Output model class should be decorated with #ObjectType() and then all properties of that class will decorated with #Field().
For any one who is using a custom output model class and NOT an entity(sequelize, typeorm, prisma etc). Because I was using database entity first, everything was working fine till I moved to a more customized output model.
One more case would be someone using class A as output and class B is used within A
export class A{
id: number;
name:string;
childProperty: B
. . . . .
}
export class B{
prop1:string;
prop2:string;
}
In that case class B should also be decorated with #ObjectType and fields (prop1 , prop2 ) should be also be decorated with #Field as well.
I am relatively new to typescript and NestJS framework. Currently I would like to implement a pagination mechanism for all models in my application. In the current api project I am using NestJS with mongoose.
My user schemma is the following
export const UserSchema = new mongoose.Schema({
firstName: String,
lastName: String,
email: String,
phone: String,
password: {
type: String
}
});
UserSchema.plugin(mongoosePaginate);
UserSchema.plugin(passportLocalMongoose, {
usernameField: 'email',
});
My user interface is the following:
export interface IUser extends PassportLocalDocument {
readonly firstName: string;
readonly lastName: string;
readonly email: string;
readonly phone: string;
readonly password: string;
}
And my userService is the following:
#Injectable()
export class UsersService implements IUsersService {
constructor(#InjectModel('User') private readonly userModel: PassportLocalModel<IUser>) {
}
async findAll(): Promise<IUser[]> {
return await this.userModel.find().exec();
}
I would like to add the mongoose-paginate functionality trough my IUser interface, so i can access it in the service via this.userModel.paginate.
I mention that I installed : "#types/mongoose-paginate": "^5.0.6" and "mongoose-paginate": "^5.0.3", and I can import PaginateModel from mongoose.
I guess the IUser interface should look something like this :
export interface IUser<T extends PassportLocalDocument > extends PaginateModel<T> {} but I am not sure, nor how to instantiate it when injecting into service.
Waiting ur responses guys, and Thanks! :D
This is a snippet for those who are using mongoose-paginate plugin with nestjs. You can also install #types/mongoose-paginate for getting the typings support
Code for adding the paginate plugin to the schema:
import { Schema } from 'mongoose';
import * as mongoosePaginate from 'mongoose-paginate';
export const MessageSchema = new Schema({
// Your schema definitions here
});
// Register plugin with the schema
MessageSchema.plugin(mongoosePaginate);
Now in the Message interface document
export interface Message extends Document {
// Your schema fields here
}
Now you can easily get the paginate method inside the service class like so
import { Injectable } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { PaginateModel } from 'mongoose';
import { Message } from './interfaces/message.interface';
#Injectable()
export class MessagesService {
constructor(
// The 'PaginateModel' will provide the necessary pagination methods
#InjectModel('Message') private readonly messageModel: PaginateModel<Message>,
) {}
/**
* Find all messages in a channel
*
* #param {string} channelId
* #param {number} [page=1]
* #param {number} [limit=10]
* #returns
* #memberof MessagesService
*/
async findAllByChannelIdPaginated(channelId: string, page: number = 1, limit: number = 10) {
const options = {
populate: [
// Your foreign key fields to populate
],
page: Number(page),
limit: Number(limit),
};
// Get the data from database
return await this.messageModel.paginate({ channel: channelId }, options);
}
}
I approached the situation in a different matter. I've created 2 interfaces, 1 for registration/authentication and the other for data manipulation.
Once u import the PaginateModel you have to extend your interface with Document.
export interface IUser extends Document
Afterwards when you inject it into your service:
#InjectModel('User') private readonly userModel: PaginateModel<IUser>
And finally, in your service interface and service implementation, change the return type like this:
async findAll(yourParams: YourParamsDto): Promise<PaginateResult<IUser>>
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);