Mongoose & TypeScript - Property '_doc' does not exist on type 'IEventModel' - mongodb

I'm learning some JavaScript backend programming from a course I'm taking. It focuses on ExpressJS, MongoDB, and GraphQL. Because I like making things more challenging for myself, I decided to also brush up on my TypeScript while I'm at it by doing all the coursework in TypeScript.
Anyway, so I'm using verison 5.5.6 of mongoose and #types/mongoose. Here is my interface for the type of the DB record:
export default interface IEvent {
_id: any;
title: string;
description: string;
price: number;
date: string | Date;
}
Then I create the Mongoose Model like this:
import { Document, Schema, model } from 'mongoose';
import IEvent from '../ts-types/Event.type';
export interface IEventModel extends IEvent, Document {}
const eventSchema: Schema = new Schema({
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
date: {
type: Date,
required: true
}
});
export default model<IEventModel>('Event', eventSchema);
Lastly, I have written the following resolver for a GraphQL mutation:
createEvent: async (args: ICreateEventArgs): Promise<IEvent> => {
const { eventInput } = args;
const event = new EventModel({
title: eventInput.title,
description: eventInput.description,
price: +eventInput.price,
date: new Date(eventInput.date)
});
try {
const result: IEventModel = await event.save();
return { ...result._doc };
} catch (ex) {
console.log(ex); // tslint:disable-line no-console
throw ex;
}
}
My problem is that TypeScript gives me an error that "._doc" is not a property on "result". The exact error is:
error TS2339: Property '_doc' does not exist on type 'IEventModel'.
I can't figure out what I'm doing wrong. I've reviewed the documentation many times and it seems that I should have all the correct Mongoose properties here. For the time being I'm going to add the property to my own interface just to move on with the course, but I'd prefer help with identifying the correct solution here.

This might be a late answer but serves for all that come searching this.
inteface DocumentResult<T> {
_doc: T;
}
interface IEvent extends DocumentResult<IEvent> {
_id: any;
title: string;
description: string;
price: number;
date: string | Date;
}
Now when you call for (...)._doc , _doc will be of type _doc and vscode will be able to interpert your type. Just with a generic declaration. Also instead of creating an interface for holding that property you could include it inside IEvent with the type of IEvent.

This is something that I do when I always use typescript alongside mongoose,
first things first we should define interfaces for the schema and model:
export interface IDummy {
something: string;
somethingElse: string;
}
export interface DummyDocument extends IDummy, mongoose.Document {
createdAt: Date;
updatedAt: Date;
_doc?: any
}
second we should create out schema:
const DummySchema = new mongoose.Schema<DummyDocument>({
something: String,
somethingElse: String,
})
finally we are going to use export model pattern for exporting our model as a module from file:
export const DummyModel = mongoose.model<DummyDocument>
Now the problem has fixed and you are not going to see the typescript error, we have manually attached the _doc to our model with generics that the aid of generics.

interface MongoResult {
_doc: any
}
export default interface IEvent extends MongoResult {
_id: any;
title: string;
description: string;
price: number;
date: string | Date;
}
Then you still have to deal with casting the _doc back to your own IEvent...

add _doc with type any to your custom Model interface
interface IUser extends Document {
...
_doc: any
}
a full example is here https://github.com/apotox/express-mongoose-typescript-starter

The _doc field will be a circular reference. So an easy way to go about it is to simply do something like this.
It also avoids infinite circular references by omitting itself in the child record.
No interface extension is required!
export default interface IEvent {
_doc: Omit<this,'_doc'>;
}

For some reasons, the structure of the return type is not included in #types/mongoose lib. So each time you want to de-structure the return object you get an error that the variable in not definition in the interface signature of both document and your custom types. That should be some sort of bug i guess.
The solution is to return the result itself, that will automatically return the data defined in interface (IEvent) without the meta data .
...
try {
const result = await event.save();
return result;
} catch (ex) {
throw ex;
}
...

Related

Can not Query all users because of MongoDB id

I am coding a CRUD API built in TypeScript and TypeGoose.
I get an error saying,
CannotDetermineGraphQLTypeError: Cannot determine GraphQL output type for '_id' of 'User' class. Is the value, that is used as its TS type or explicit type, decorated with a proper decorator or is it a proper output value?
I have a User entity.
import { Field, ObjectType } from 'type-graphql';
import { ObjectId } from 'mongodb';
import { prop as Property, getModelForClass } from '#typegoose/typegoose';
#ObjectType()
export class User {
#Field()
readonly _id: ObjectId;
#Field()
#Property({ required: true })
email: string;
#Field({ nullable: true })
#Property()
nickname?: string;
#Property({ required: true })
password: string;
constructor(email: string, password: string) {
this.email = email;
this.password = password;
}
}
export const UserModel = getModelForClass(User);
And this is how my query resolver looks like.
#Query(() => [User])
async users() {
const users = await UserModel.find();
console.log(users);
return users;
}
How can I solve this? It seems to be like TypeGraphQL doesn't understand what the MongoDB ID is?
Im not sure about this, but maybe ObjectId.toString() help you.
MongoDB doc about ObjectId.toString()

NestJS with mongoose schema, interface and dto approach question

I am new into nestJS and mongoDB and its not clear for me why do we need to declare DTO, schema and interface for each collection we want to save in our mongoDB. IE. I have a collection (unfortunately I've named it collection but it does not matter) and this is my DTO:
export class CollectionDto {
readonly description: string;
readonly name: string;
readonly expiration: Date;
}
interface:
import { Document } from 'mongoose';
export interface Collection extends Document {
readonly description: string;
readonly name: string;
readonly expiration: Date;
}
and schema:
import * as mongoose from 'mongoose';
export const CollectionSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
description: {
type: String,
required: false,
},
expiration: {
type: String,
required: true,
}
});
My doubt is that do we really need as many as three objects with almost the same contents? It looks strange at first sight.
I was working with mongoose a lot in plain nodejs basis and as well I'm starting to work with NestJS. Mongoose defines two things so that you can use mongodb to create, query, update and delete documents: Schema and Model. You already have your schema, and for model in plain mongoose should be as:
import * as mongoose from 'mongoose';
export const CollectionSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
description: {
type: String,
required: false,
},
expiration: {
type: String,
required: true,
}
});
const Collection = mongoose.model('collections', CollectionSchema);
Collection here will be mongoose model. So far so good.
In NestJs, and if you are going to follow API best practices, you will use a DTO (Data Transfer Object). NestJs in doc mention that is preferable to use classes than interfaces, so you don't need interfaces here. When you define Mongoose schema, you can also define Model/Schema:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
export type CollectionDocument = Collection & Document;
#Schema()
export class Collection {
#Prop()
name: string;
#Prop()
description: number;
#Prop()
expiration: string;
}
export const CollectionSchema = SchemaFactory.createForClass(Collection);
And for your services and controllers you use both (model and DTO):
import { Model } from 'mongoose';
import { Injectable } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { Collection, CollectionDocument } from './schemas/collection.schema';
import { CollectionDto } from './dto/collection.dto';
#Injectable()
export class CollectionService {
constructor(#InjectModel(Collection.name) private collectionModel: Model<CollectionDocument>) {}
async create(createColDto: CollectionDto): Promise<Collection> {
const createdCollection = new this.collectionModel(createColDto);
return createdCollection.save();
}
async findAll(): Promise<Collection[]> {
return this.collectionModel.find().exec();
}
}
After this, you can user Swagger to automatic doc of your APIs.
NestJS Mongo Techniques

Why DTOs are not throwing Validation error in nestjs?

I am using DTO in my code, and I am getting the response as expected but in code DTOs are not throwing error for example
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
In this name, age, the breed is a required field and each has their data type but while running on the postman when I am not passing all the required field or only one field into postman body I am not getting any errors like age is required if I have passed other two fields or I have given value of the parameter not according to data type like:- age : twenty five then also it should throw error but I am not getting.
So, This is class created for
import { ApiProperty } from '#nestjs/swagger';
export class Cat {
#ApiProperty({ example: 'Kitty', description: 'The name of the Cat' })
name: string;
#ApiProperty({ example: 1, description: 'The age of the Cat' })
age: number;
#ApiProperty({
example: 'Maine Coon',
description: 'The breed of the Cat',
})
breed: string;
}
This is controller in which I am importing class and Dto.
import { Body, Controller, Get, Param, Post } from '#nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
} from '#nestjs/swagger';
import { CatsService } from './cats.service';
import { Cat } from './classes/cat.class';
import { CreateCatDto } from './dto/create-cat.dto';
#ApiBearerAuth()
#ApiTags('cats')
#Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
#Post()
#ApiOperation({ summary: 'Create cat' })
#ApiResponse({ status: 403, description: 'Forbidden.' })
async create(#Body() createCatDto: CreateCatDto): Promise<Cat> {
return this.catsService.create(createCatDto);
}
}
I don't know why you selected nestjs-swagger tag, DTO by itself will not validate inputs, maybe you need to use a ValidationPipe with the class-validator package as suggested on docs https://docs.nestjs.com/techniques/validation#validation
It's as simple as putting a decorator on your code now:
import { IsEmail, IsNotEmpty } from 'class-validator';
export class CreateCatDto {
#IsNotEmpty()
#IsString()
readonly name: string;
#IsNotEmpty()
#IsInt()
readonly age: number;
#IsNotEmpty()
readonly breed: string;
You can see all the items here: https://github.com/typestack/class-validator#validation-decorators
And if you want to sanitize the request body, you should use a serializer to help:
https://docs.nestjs.com/techniques/serialization#serialization
This will show or hide your DTO properties based on decorators of each field. You need to install class-transformer package.
import { Exclude } from 'class-transformer';
export class UserEntity {
id: number;
firstName: string;
lastName: string;
#Exclude()
password: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
It's important to remember that interceptors will run on your request and response.

How do I refer to the _id field in a mongodb database via a REST API GET call?

This is hard to explain, so please bear with me.
I have a Nestjs-based server that is using MongoDB as the back end. Here are the three entries from the mongodb collection:
{"_id":{"$oid":"5e87ef7a9cf8648fac6b9f1e"},"complete":false,"editMode":false,"createdAt":{"$date":{"$numberLong":"1585966970857"}},"createdBy":"user","isDeleted":false,"title":"Make a birthday cake","note":"Make sure she poops."}
{"_id":{"$oid":"5e87f081237c70a6782d6c2a"},"createdAt":{"$date":{"$numberLong":"1585967233825"}},"createdBy":"user","isDeleted":false,"complete":false,"editMode":false,"title":"Clean the kitchen","note":"Use Lysol"}
{"_id":{"$oid":"5e87f73be81d7e0061311187"},"createdAt":{"$date":{"$numberLong":"1585968955971"}},"createdBy":"user","isDeleted":false,"complete":false,"editMode":false,"title":"Walk the dog","note":"Make sure she poops."}
Here is my model:
#Entity()
export class Todo {
#ObjectIdColumn()
#Transform(value => value.toString(), { toPlainOnly: true })
id: ObjectID;
#Column({ length: 100 })
title: string;
#Column({ length: 5000 })
note: string;
#Column()
complete: boolean;
#Column()
editMode: boolean;
#Exclude() #Column() createdAt: Date = new Date();
#Exclude() #Column() createdBy: string = 'user';
#Exclude() #Column() isDeleted: boolean = false;
}
Here is my GET-er stuff:
#Get(':id')
async getTodo(#Param('id') id: number) {
return this.todosService.getTodo(id);
}
async getTodo(id: number): Promise<Todo | undefined> {
return this.todosRepository.findOne(id, {
where: {
isDeleted: false,
},
});
}
I would like to run a REST GET call to retrieve one of the documents, say the third one. Therefore I call in my browser:
http://localhost:3000/todos/5e87f73be81d7e0061311187
Well, this returns the first document. In fact, anything I call only returns the first document.
What should my GET call be to get the third item?
I can provide any further information that might be needed.
Firstly, i think the id parameter (id: number), should be a string.
Also, findone takes all the querying parameter in first argument itself ( atleast in JS), so it should be like todoRepository.findOne({_id .$oid: string, isDeleted: false }).
Can you tell me what's $oid? ObjectId type? I haven't worked with nest.js/ typescript. Depending upon this you might want to improve the $oid in query argument.
Here's the answer: Make the id parameter an ObjectID type everywhere in the application.
For instance:
#Get(':id')
async getTodo(#Param('id') id: ObjectID) {
return this.todosService.getTodo(id);
}
async getTodo(id: ObjectID): Promise<Todo | undefined> {
return this.todosRepository.findOne(id, {
where: {
isDeleted: false,
},
});
}
Once I did that, it all worked as expected.

MongoDB and TypeScript: Decouple a domain entity's id type from MongoDB's ObjectID

Inside my MongoDB repositories, entities have an _id: ObjectID type to be handled properly. However, I would like my domain entities to have a simple id: string attribute to avoid any dependencies on any database or framework. The solution I came up with so far looks as follows:
export interface Book {
id: string;
title: string;
}
// A MongodbEntity<Book> would now have an _id instead of its string id
export type MongodbEntity<T extends { id: string; }> = Omit<T, 'id'> & { _id: ObjectID; };
In my repository this would work:
async findOneById(id: string): Promise<Book | null> {
const res = await this.collection.findOneById({_id: new ObjectId(id)});
return res ? toBook(res) : null;
}
function toBook(dbBook: MongodbEntity<Book>): Book {
const {_id, ...rest} = dbBook;
return {...rest, id: _id.toHexString() };
}
What doesn't work is to make this behavior generic. A converter function like this:
function toDomainEntity<T extends {id: string}>(dbEntity: MongoDbEntity<T>): T {
const {_id, ...rest} = dbEntity;
return {...rest, id: _id.toHexString() };
}
leads to an error described here.
What I am looking for is either a working solution for the generic toDomainEntity function or a different (generic) approach that would let me decouple my domain entity types from MongoDB's _id: ObjectID type.