PassportLocalDocument and PaginateModel on the same interface? - node.js

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>>

Related

Sequelize Associations Not Working with Typescript

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 :/

Attach user id from access token using DTO

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

Typescript Sequelize, M:M relation, query only returns single entry

I'm using NestJs framework with Sequelize Typescript for Node where I'm trying to create a many to many relation between a user and a webpage where many users can have the same site in their favorites.
Now my problem is that when I make the query it limits the result to a single entry, while querying the database directly with the exact same query returns all expected entries.
This is my NestJs favorites entity where I define the favorite table:
// favorite.entity.ts
import { Table, Column, Model, PrimaryKey, ForeignKey, BelongsTo, NotNull } from "sequelize-typescript";
import { IDefineOptions } from "sequelize-typescript/lib/interfaces/IDefineOptions";
import { UserEntity } from "../users/user.entity";
import { SiteEntity } from "../sites/site.entity";
const tableOptions: IDefineOptions = {
timestamp: true,
tableName: "favorites",
schema: process.env.DB_SCHEMA,
} as IDefineOptions;
#Table(tableOptions)
export class FavoriteEntity extends Model<FavoriteEntity> {
#BelongsTo(() => UserEntity)
user: UserEntity;
#ForeignKey(() => UserEntity)
#PrimaryKey
#NotNull
#Column
userId: number;
#BelongsTo(() => SiteEntity)
site: SiteEntity;
#ForeignKey(() => SiteEntity)
#PrimaryKey
#NotNull
#Column
siteId: number;
}
And my service where I make the Sequelize query:
// favorite.service.ts
import { Inject, Injectable } from "#nestjs/common";
import { Model } from "sequelize-typescript";
import { IFavoriteService } from "./interfaces";
import { FavoriteEntity } from "./favorite.entity";
#Injectable()
export class FavoriteService implements IFavoriteService {
constructor(
#Inject("FavoriteRepository") private readonly favoriteRepository: typeof Model,
#Inject("SequelizeInstance") private readonly sequelizeInstance,
) {}
public async findByUserId(userId: number): Promise<FavoriteEntity | null> {
return await FavoriteEntity.scope().find<FavoriteEntity>({
logging: console.log,
where: { userId },
});
}
}
The logged out SQL statement is:
Executing (default): SELECT "userId", "siteId" FROM "public"."favorites" AS "FavoriteEntity" WHERE "FavoriteEntity"."userId" = '1';
And results in a single entry as result (there are many rows in the db)...
{
"userId": 1,
"siteId": 1650
}
Have I made some mistake in my entity or might it be in my sequelize query?
Thanks!
what is your result if you use findAll instead of find.but i am agree the query looks nice
mottosson are you sure the same sql query returns many rows? Because it will be odd.

Creating mongoose models with typescript - subdocuments

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

Typescript and Mongoose Model

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

Resources