I'm trying to get nested subdocuments array using Typegoose.
Before refactoring with Typegoose, I had this working code with mongoose :
Interface :
export interface IFamily extends Document {
name: string;
products: IProduct[];
}
Schema :
const familySchema: Schema = new Schema({
name: { type: String },
products: [{ type: Schema.Types.ObjectId, ref: 'Product' }]
});
When I did Family.findById('5f69aa0a56ca5426b44a86c5'), I had an array of Product ObjectId in my JSON result.
After refoctoring, I use Typegoose :
Class :
#modelOptions({ schemaOptions: { collection: 'families' } })
export class Family {
#prop({ type: Schema.Types.ObjectId })
public _id?: string;
#prop({ type: String, required: false })
public name?: string;
#prop({ ref: () => Product, required: true, default: [] })
public products!: Product[];
}
When I do :
getModelForClass(Family).findById('5f69aa0a56ca5426b44a86c5')
property "products" with an array of ObjectId is not in the result (property is absent) :
{
"_id": "5f69aa0a56ca5426b44a86c5",
"name": "Fourniture"
}
I can't figue how to make that working. I think the problem is in the family class #prop(ref). I saw some example where peoples used #arrayProp but now is deprecated.
I find documentation about ref with a simple object but not with an array of object with version 5.9.1 of Typegoose.
Thanks
aside from using an old version of typegoose with new "syntax"
this is how it is supposed to look in typegoose (7.4)
#modelOptions({ schemaOptions: { collection: 'families' } })
export class Family {
#prop()
public _id?: string;
#prop()
public name?: string;
#prop({ ref: () => Product, required: true, default: [] })
public products!: Ref<Product>[]; // if Product's "_id" is also "string", then it would need to be ": Ref<Product, string>[];"
}
Related
I'm using Typegoose v9.3.1
I need to create a reference for another collection with non _id field using Typegoose.
For example i'm having the following models.
Class collection
export class Class {
#prop({ required: true })
public _id!: string;
#prop({ required: true })
public grade!: Source;
#prop({ autopopulate: true, ref: Student, type: String })
public students!: Ref<Student, string>[];
}
Student Collection
export class Student {
#prop({ required: true })
public rollNo!: string;
#prop({ required: true })
public name!: string;
}
From the above example Class collection referencing the student collection based on the _id field (by default) of Student collection. But i need to create a reference based on the rollNo field(non _id field).
Thanks in Advance!
The only thing i know of for this case are virtual populates:
Mongoose: Virtual Populate
Typegoose: Virtual Populate
I am not aware of any method to change the field the prop-option ref uses, so i can only provide a virtual populate example.
Note: this example may not work in all cases, like if you have specific data layout requirements.
Example:
export class Class {
#prop({ required: true })
public _id!: string;
#prop({ required: true })
public grade!: Source;
#prop({ autopopulate: true, ref: () => Student, foreignField: "classId localField: "_id" })
public students!: Ref<Student, string>[];
}
export class Student {
#prop({ required: true, ref: () => Class }) // this could also be done without "ref" and just setting the type to "ObjectId"
public classId: Ref<Class>;
#prop({ required: true })
public rollNo!: string;
#prop({ required: true })
public name!: string;
}
If the above example does not work in your case, then i think your only option is to use a manual query / aggregation.
I'm building the back-end side of a personal application, and I have two particular models/schemas. One if for Products an another for Orders. I want to do the following:
The Orders need to have the following array with this structure:
products: [
{
product: string;
quantity: number;
}
]
The product should be an ObjectId of mongo, and this needs a reference for a 'Product' model.
How I can reach this? I don't really know how to "type" this with the #Prop() decorator.
#Prop({
// HOW I DO THIS?
})
products: [{ quantity: number; product: mongoose.Types.ObjectId }];
This is my Order Schema:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import * as mongoose from 'mongoose';
import { Document } from 'mongoose';
export type OrderDocument = Order & Document;
#Schema()
export class Order {
#Prop({ type: String, required: true })
name: string;
#Prop({ type: Number, min: 0, required: true })
total: number;
#Prop({
type: String,
default: 'PENDING',
enum: ['PENDING', 'IN PROGRESS', 'COMPLETED'],
})
status: string;
#Prop({
// HOW I DO THIS?
})
products: [{ quantity: number; product: mongoose.Types.ObjectId }];
#Prop({
type: mongoose.Schema.Types.ObjectId,
ref: 'Customer',
required: true,
})
customer: mongoose.Types.ObjectId;
#Prop({
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
})
owner: mongoose.Types.ObjectId;
#Prop({ type: Date, default: Date.now() })
createdAt: Date;
}
export const OrderSchema = SchemaFactory.createForClass(Order);
#Prop({
type:[{quantity:{type:Number}, product:{type:Schema.Types.ObjectId}}]
})
products: { quantity: number; product: Product }[];
The best way to achieve both reference population and mongoose schema validation is to create a subschema for the nested entity.
import { Document, SchemaTypes, Types } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
#Schema({ _id: false, versionKey: false })
class OrderProduct {
#Prop({ required: true })
quantity: number;
#Prop({ type: [SchemaTypes.ObjectId], ref: 'Product', required: true })
product: Product[];
}
const OrderProductSchema = SchemaFactory.createForClass(OrderProduct);
#Schema()
export class Order {
// other order schema props ...
#Prop([{ type: OrderProductSchema }])
products: OrderProduct[];
}
This is the fist time I use MongoDB with Mongoose and Typescript. I am defining an interface for each model of my database.
Should I add the _id field to the interfaces I create, like I am doing in this code snippet?
import mongoose, { Model, Document, ObjectId } from "mongoose";
export interface IUser extends Document {
_id: ObjectId;
name: string;
email: string;
password: string;
created: Date;
isPremium: boolean;
img?: string;
applicantProfile?: string;
businessProfile?: string;
}
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true },
isPremium: { type: Boolean, required: true, default: true },
created: { type: Date, required: true, default: Date.now },
img: String,
applicantProfile: String,
businessProfile: String,
});
export const User: Model<IUser> = mongoose.model("User", userSchema)
The tutorials I have read about Mongo + Typescript don't do this, but I have found that, if I don't declare the _id field in my interface, then, when I try to access the _id field, it has a value of any, which I don't like.
I'd like to know how you create the interfaces of your models and whether adding the _id field to the interface is a good practice.
I'm using mongoose with typescript and today I stepped into some confusion with Model and Document.
Here is definition of my subdocument.
export interface IUserUnitResource {
serialNumber: string;
role: string;
name?: string;
alarms: IUserAlarmResource[];
}
export interface IUserUnitEntity extends IUserUnitResource, mongoose.Document {
alarms: mongoose.Types.DocumentArray<IUserAlarmEntity>;
}
export const userUnitSchema = new mongoose.Schema(
{
serialNumber: { type: String, required: true, index: true },
role: { type: String, default: 'user' },
name: String,
alarms: { type: [userAlarmSchema], default: [] },
},
{ _id: false }
);
In mongoose documentation they say:
An instance of a model is called a document.
So I assumed that i can call ownerDocument method on that subdocument. But typescript gives me an error:
Property 'ownerDocument' does not exist on type 'IUserUnitEntity'.
But if i try to call that method using any i get desired document.
(uu as any).ownerDocument()
Do I have the typings wrong, or it's a bug?
I use NestJs + Typegoose. How to replace _id to id in NestJs + Typegoose? I didn't find a clear example. I've tried something but without any results.
#modelOptions({
schemaOptions: {
collection: 'users',
},
})
export class UserEntity {
#prop()
id?: string;
#prop({ required: true })
public email: string;
#prop({ required: true })
public password: string;
#prop({ enum: UserRole, default: UserRole.User, type: String })
public role: UserRole;
#prop({ default: null })
public subscription: string;
}
#Injectable()
export class UsersService {
constructor(
#InjectModel(UserEntity) private readonly userModel: ModelType<UserEntity>,
) {}
getOneByEmail(email: string) {
return from(
this.userModel
.findOne({ email })
.select('-password')
.lean(),
);
}
}
Another way to update the default _id to id is by overriding the toJSON method in the modelOptions decorator.
#modelOptions({
schemaOptions: {
collection: 'Order',
timestamps: true,
toJSON: {
transform: (doc: DocumentType<TicketClass>, ret) => {
delete ret.__v;
ret.id = ret._id;
delete ret._id;
}
}
}
})
#plugin(AutoIncrementSimple, [{ field: 'version' }])
class TicketClass {
#prop({ required: true })
public title!: string
#prop({ required: true })
public price!: number
#prop({ default: 1 })
public version?: number
}
export type TicketDocument = DocumentType<TicketClass>
export const Ticket = getModelForClass(TicketClass);
using typegoose with class-transformer:
import * as mongoose from 'mongoose';
import { Expose, Exclude, Transform } from 'class-transformer';
#Exclude()
// re-implement base Document to allow class-transformer to serialize/deserialize its properties
// This class is needed, otherwise "_id" and "__v" would be excluded from the output
export class DocumentCT {
#Expose({ name: '_id' })
// makes sure that when deserializing from a Mongoose Object, ObjectId is serialized into a string
#Transform((value: any) => {
if ('value' in value) {
return value.value instanceof mongoose.Types.ObjectId ? value.value.toHexString() : value.value.toString();
}
return 'unknown value';
})
public id: string;
#Expose()
public createdAt: Date;
#Expose()
public updatedAt: Date;
}
i can say it is underlying mongoose behaivor
You can send JSON with 'id' instead of '_id' ,a virtual property on all your models is a pretty safe and easy way to
do it.
An example:
export const NotesSchema = new Schema({
title: String,
description: String,
});
NotesSchema.virtual('id')
.get(function() {
return this._id.toHexString();
});
or you can create a toClient() method on your models where you do this. It's also a good place to rename/remove other attributes you don't want to send to the client:
NotesSchema.method('toClient', function() {
var obj = this.toObject();
//Rename fields
obj.id = obj._id;
delete obj._id;
return obj;
});