NestJS with mongoose schema, interface and dto approach question - mongodb

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

Related

Is there a native way to ensure a document exists in database before creating a subdocument with NestJS/Mongo?

I'm writing a NestJS api using MongoDB (and Mongoose) for the database. I have a parent document: "Writer", which may have subdocuments: "Book".
Here is the "Writer" document:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
export type WriterDocument = Writer & Document;
#Schema()
export class Writer {
#Prop({
required: [true, "Name is required and can't be empty!"],
unique: true
})
name: string;
}
export const WriterSchema = SchemaFactory.createForClass(Writer);
And the "Book" subdocument:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { Writer } from 'src/writer/schemas/writer.schema';
export type BookDocument = Book & Document;
#Schema()
export class Book {
#Prop({
required: true,
type: mongoose.Schema.Types.ObjectId,
ref: Writer.name
})
writer: Writer;
#Prop({ required: true })
name: string;
}
export const BookSchema = SchemaFactory.createForClass(Book);
I want to ensure the writer exists before inserting any book for him in the database, but if I'm right, by default, the existence of the writer is not checked, and a book can be inserted at any time without having a real writer.
I just started using NestJS and MongoDB (with Mongoose), and I don't really know how to do it. Maybe this is the way to do with these technologies but I would rather prevent this from happening.
How can I do that? With Middleware? Guard? Pipe? Something else?
Thanks in advance.

Is there any way to get the list of validators used in moongoose schema?

I want to get the list of validators that is used in a moongoose schema? Something like
const userModel = {
firstName: {
type:String,
required: true
}
}
// is there any method to get validations like
console.log(userModel.getValidators())
Output:
{
firstName: {required: true ....etc},
}
Once you setup your model using the SchemaFactory.createForClass method from a class with a #Schema decorator as shown in the docs, you can export the schema. If you then, import the schema and access its obj property, you can extract information about the field.
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
export type CatDocument = Cat & Document;
#Schema()
export class Cat {
#Prop({ required: true })
name: string;
#Prop()
age: number;
#Prop()
breed: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
import { CatSchema } from './cat.schema';
console.log(CatSchema.obj.name.required); // true
console.log(CatSchema.obj.age.type.name); // 'Number'

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

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

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;
}
...

Loopback 4 - HasMany relation included in fields

I am trying to setup the relation HasMany with the new Loopback 4 framework.
I have the following model:
import {Entity, model, property, belongsTo, hasMany} from
'#loopback/repository';
import {User} from "./user.model";
import {OrderProduct} from "./order-product.model";
#model({
name: 'sales_order'
})
export class Order extends Entity {
#property({
type: 'number',
id: true,
required: true,
})
id: number;
#property({
type: 'number',
required: true,
})
total_amount: number;
#belongsTo(() => User)
user_id: number;
#hasMany(() => OrderProduct, {keyTo: 'order_id'})
products?: OrderProduct[];
constructor(data?: Partial<Order>) {
super(data);
}
}
And the repository as follow:
import {DefaultCrudRepository, repository, HasManyRepositoryFactory, BelongsToAccessor} from '#loopback/repository';
import {Order, OrderProduct, User} from '../models';
import {DbDataSource} from '../datasources';
import {inject, Getter} from '#loopback/core';
import {OrderProductRepository} from "./order-product.repository";
import {UserRepository} from "./user.repository";
export class OrderRepository extends DefaultCrudRepository<
Order,
typeof Order.prototype.id
> {
public readonly user: BelongsToAccessor<
User,
typeof Order.prototype.id
>;
public readonly products: HasManyRepositoryFactory<
OrderProduct,
typeof Order.prototype.id
>;
constructor(
#inject('datasources.db') dataSource: DbDataSource,
#repository.getter(OrderProductRepository)
getOrderProductRepository: Getter<OrderProductRepository>,
#repository.getter('UserRepository')
userRepositoryGetter: Getter<UserRepository>,
) {
super(Order, dataSource);
this.products = this._createHasManyRepositoryFactoryFor(
'products',
getOrderProductRepository,
);
this.user = this._createBelongsToAccessorFor(
'user_id',
userRepositoryGetter,
);
}
}
When I do for example a get on orders, I have the errors: 500 error: column "products" does not exist and in digging a bit more, I can see that the SQL is trying to retrieve the fields products where it is just a relation.
Anybody have an idea if I am doing something wrong?
I am using pg as DB.
I believe this is a bug in LoopBack 4. When you decorate a class property with #hasMany, the decorator defines a model property under the hood. See here:
export function hasMany<T extends Entity>(
targetResolver: EntityResolver<T>,
definition?: Partial<HasManyDefinition>,
) {
return function(decoratedTarget: Object, key: string) {
property.array(targetResolver)(decoratedTarget, key);
// ...
};
}
When the connector is querying the database, it's trying to include the column products in the query, because it thinks products is a property.
The problem is already tracked by https://github.com/strongloop/loopback-next/issues/1909, please consider upvoting the issue and joining the discussion.