Mongoose with typescript? - node.js

I have a project in nodejs and typescript. I'm using mongoose to connect to a mongoDb database. My code looks like this
import { Schema, Document, Model } from 'mongoose';
import * as mongoose from 'mongoose';
export interface IProblem extends Document {
problem: string;
solution: string;
}
const ProblemSchema = new Schema({
problem: { type: String, required: true },
solution: { type: String, required: true },
});
export async function findOneByProblem(
this: IProblemModel,
{ problem, solution }: { problem: string; solution: string }
): Promise<IProblem> {
const record = await this.findOne({ problem, solution });
return record;
}
export default mongoose.model('Problem', ProblemSchema);
ProblemSchema.statics.findOneByProblem = findOneByProblem;
export interface IProblemModel extends Model<IProblem> {
findOneByProblem: (
this: IProblemModel,
{ problem, solution }: { problem: string; solution: string }
) => Promise<IProblem>;
}
However, at these lines
const record = await this.findOne({ problem, solution });
return record;
I get a compiler error saying this
TS2322: Type 'IProblem | null' is not assignable to type 'IProblem'.   Type 'null' is not assignable to type 'IProblem'.
Am I missing something?

Your type for findOneByProblem is wrong – after all, it's possible that you don't find an IProblem instance, and the result is null.
The correct type is
Promise<IProblem | null>
– or you could internally if(problem === null) throw new Error("No Problem found"); or similar in the function if you don't want to change the type.

Related

type error in using typescript and mongodb findOneAndUpdate method

This is my mongodb collection model
export class User {
constructor(
public _id: ObjectId,
public username: string,
public phone: string,
public password: string,
public pages: Array<ObjectId>
) { }
}
I want to add new ObjectId into pages:
const pageID = new ObjectId()
const result1 = await userCollection.findOneAndUpdate({
_id: new ObjectId(userID)
}, {
$push: {
pages: pageID
}
})
And there is an error near $push
Type '{ pages: ObjectId; }' is not assignable to type 'PushOperator<Document>'.
Type '{ pages: ObjectId; }' is not assignable to type 'NotAcceptedFields<Document, readonly any[]>'.
Property 'pages' is incompatible with index signature.
Type 'ObjectId' is not assignable to type 'undefined'.
I found lots of examples in js and shell, but none in ts. Can anyone help me with that?
I encountered the same problem after updating to MongoDB driver v4.9.0. The solution is to cast your update object to Record<string, any>:
const pageID = new ObjectId();
const updateObj: Record<string, any> = {
pages: pageID
};
const result1 = await userCollection.findOneAndUpdate(
{ _id: new ObjectId(userID) },
{ $push: updateObj }
);
The reason is the following PR: 3349, which changed the definition of MatchKeysAndValues, and affected the update argument of the following functions: updateOne, updateMany, findOneAndUpdate, update.

Mongoose with Typescript: Trying to split schema, methods and statics in seperate files, problem with this has type any and is hidden by container

i'm trying to split up my single-files mongoose schemas with statics and methods.
(I found this tutorial for splitting: https://medium.com/swlh/using-typescript-with-mongodb-393caf7adfef ) I'm new to typescript but love the benefits it gives while coding.
I've splitted my user.ts into:
user.schema.ts
user.model.ts
user.types.ts
user.statics.ts
user.methods.ts
When i change this lines in my schema file:
UserSchema.statics.findUserForSignIn = async function findUserForSignIn(
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ email: email });
if (!user) {
return user;
} else {
return user;
}
}
to UserSchema.statics.findUserForSignIn = findUserForSignIn;
and copy the Function findUserForSignIn to user.statics.ts, Typescript says "'this' implicitly has type 'any' because it does not have a type annotation" and "An outer value of 'this' is shadowed by this container."
So, how to add this properly? If i add this to findUserForSignIn with IUserModel as Type, add null to Promise return type it would nearly work:
export async function findUserForSignIn(
this: IUserModel,
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ "person.email": email });
return user;
}
And if i add this to receiving function parameters: users gets to type IUserDocument, before it was any. I think its nice to have typeclear, not just any.
But: in user.schema.ts the UserSchema.statics.findUserForSignIn gets a red line from typescript. Type can not be assigned to other type. The signature of this is not identical.
If i change the type of this to any, all is okay. But the return is not longer from type IUserDocument. Mabye its okay if i get over an aggregation pipeline and only set the Promise-Return-Type. But that this: any gets hinted in yellow by typescript.
And, another question: if i pass this as first and email as second parameter, why is only one parameter required?
Anyone has an "how to" for me? Or can explain what i've done wrong? Or what is the best way? Or is it not possible to split statics and methods in seperate files from schema?
Original files:
user.schema.ts
import { Schema } from "mongoose";
import { PersonSchema } from "./person.schema";
import { findUserForSignIn } from "./user.statics";
import { IUserDocument } from "./user.types";
const UserSchema = new Schema<IUserDocument>({
firstname: {
type: String,
required: true,
},
lastname: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
});
UserSchema.statics.findUserForSignIn = findUserForSignIn;
export default UserSchema;
user.types.ts
import { Document, Model } from "mongoose";
import { IPerson } from "./person.types";
export interface IUser {
firstname: string;
lastname: string;
email: string;
}
export interface IUserDocument extends IUser, Document {}
export interface IUserModel extends Model<IUserDocument> {
findUserForSignIn: (email: string) => Promise<IUserDocument>;
}
user.model.ts
import { model } from "mongoose";
import UserSchema from "./user.schema";
import { IUserDocument, IUserModel } from "./user.types";
const User = model<IUserDocument>("User", UserSchema) as IUserModel;
export default User;
user.statics.ts
import { IUserDocument } from "./user.types";
export async function findUserForSignIn(
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ email: email });
if (!user) {
return user;
} else {
return user;
}
}
The only way seems to change the user.statics.ts
export async function findUserForSignIn(
this: Model<IUserDocument>,
email: string
): Promise<IUserDocument | null> {
console.log("E-Mail", email);
const user = await this.findOne({ email: email });
return user;
}
this has to be of type Model
Then code seems to be okay.

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 (Typescript): Type 'string | ParsedQs | string[] | ParsedQs[]' is not assignable to type 'Condition<string>'

Defined mongoose model with typescript interface like this:
import { Schema, model, Document } from 'mongoose';
export interface IUserInfo extends Document {
userId: string;
email: string;
}
const UserInfoSchema:Schema = new Schema<IUserInfo>({
userId: { type: String, unique: true },
email: String
}, { timestamps: true });
export const UserInfoModel = model<IUserInfo>('userInfo', UserInfoSchema);
Using UserInfoModel to get email by userId with mongoose method like this:
const user: IUserInfo = await UserInfoModel.findOne({ userId: req.query.userId });
getting this error on userId:
Type 'string | ParsedQs | string[] | ParsedQs[]' is not assignable to type 'Condition<string>'.
Type 'string[]' is not assignable to type 'Condition<string>'.
Type 'string[]' is not assignable to type 'string'.
166 const user: IUserInfo = await UserInfoModel.findOne({ userId: req.query.usreId });
~~~~~~
src/models/userInfo.ts:4:3
4 userId: string;
~~~~~~
The expected type comes from property 'userId' which is declared here on type 'FilterQuery<IUserInfo>'
I can't figure out what am I doing wrong here.
The issue is findOne expects type FilterQuery<IUserInfo> here. The type FilterQuery has pretty complex structure. But essentially boils down in your case to the fact that it expects an object where userId field must have type string.
express uses for parsing query string qs library. Where the result (req.query) of parsing query string is typed as follows:
interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] }
As you may notice while the consuming function findOne expects only a string, req.query.userId can provide a wide range of possible result types.
From typescript's perspective that's a red flag. Imaging we're strongly expecting a string and going to lowerCase it right away but receiving an array of strings instead:
function lowerCase(str: string) {
return str.toLowerCase() // totally type safe operation assuming input type
}
const stringOrArr: string | string[] = []
lowerCase(stringOrArr) // throws an error
So to feed the findOne your req.query.userId you have to narrow it's type to the acceptable by the consuming function.
you can take responsibility and just type assert it
const user: IUserInfo =
await UserInfoModel.findOne({ userId: req.query.userId as string });
or take a longer route and actually narrow it using assertion function for example:
function assertString(str: unknown): asserts str is string {
if (typeof query.userId !== 'string') throw new Error('Must be a string')
}
assertString(req.query.userId);
const user: IUserInfo =
await UserInfoModel.findOne({ userId: req.query.userId }); // no error now
Solution
export interface Person {
name: string;
last: string;
}
export class PersonDoc extends mongoose.Document implements Person {}
export type PersonModel = mongoose.Model<PersonDoc>;
const personSchema = new mongoose.Schema<PersonDoc, PersonModel, Person>({
name: { type: mongoose.SchemaTypes.String, required: true },
last: { type: mongoose.SchemaTypes.String, required: true },
})
at yow schema definition use them mongoose.SchemaTypes, instead of the normal types use usually use.

How to use objectId validation in joiful?

I tried to joiful validation using mongodb objectId.but its throwing error Property 'ObjectId' does not exist on type 'typeof import("/home/lenovo/Music/basic/node_modules/joiful/index")'
import * as jf from "joiful";
import {ObjectId} from 'mongodb';
class SignUp {
#jf.string().required()
username?: string;
#jf
.string()
.required()
.min(8)
password?: string;
#jf.date()
dateOfBirth?: Date;
#jf.boolean().required()
subscribedToNewsletter?: boolean;
#jf.ObjectId().required()
id?:ObjectId;
}
const signUp = new SignUp();
signUp.username = "rick.sanchez";
signUp.password = "wubbalubbadubdub";
const { error } = jf.validate(signUp);
Is it possible to validate objectId using joiful.
I know that this question is along time ago, and the library maintainers didn't add this validator yet, for that I created a custom decorator that uses joiful custom method to make custom validation
import * as jf from 'joiful';
import Joi from 'joi';
import { ObjectId } from 'mongodb';
export const objectIdValidationDecorator = () => jf.any().custom(({ schema }: { schema: Joi.Schema }) => {
return schema.custom((value, helpers) => {
const objectId = new ObjectId(value);
if (objectId.equals(value)) {
return objectId;
} else {
return helpers.error('any.invalid');
}
});
})
Usage:
class MyObj {
#objectIdValidationDecorator().required()
referenceId:ObjectId
}

Resources