#How to get Virtuals from Sub-Documents

21 messages · Page 1 of 1 (latest)

hard totem
#

Hello Developers,

I've an issue with the usage of virtual properties. I got one parent document (e.g. Users) which embeds another document (e.g. Stats). To do so, I've followed the instructions from documentations (https://docs.nestjs.com/techniques/mongodb#subdocuments).

The sub-document has two properties: wins and losses. With these values I want to create a virtual property on the sub-document called winRate. When I try to get the users document, the stats are included, but the winRate is still missing, even when it is defined in subdocuments schema.

Parent Definition of Sub-Document

  @Prop({ type: [RankedStat] })
  rankedStats: RankedStat[];

Virtual Property inside Sub-Document

  @Virtual({
    get: function (this: RankedStat) {
      return (this.wins / (this.wins + this.losses)) * 100;
    }
  })
  winRate: number;

I know, that I have to set toJSON: { virtuals: true } inside of the schema options, but as already mentioned, the winRate is not visible in the output. Somehow, if I define the virtual property on the parents document schema (without decorators, after schema) it is working, but honestly, I am not happy with this as I'd like to use the decorators and in my opinion, the solution below mixes responsibility.

Current working solution, but unhappy with it

  UserSchema.path('rankedStats').schema.virtual('winRate').get(function(this: RankedStat) {
    return (this.wins / (this.wins + this.losses)) * 100;
  });

Is it a bug I've figured out or do I miss something? Oh, just to clarify: I use @nestjs/mongoose and I'd like to stick with it.

hearty coral
#

Can you offer a reproduction repository? Or, at least the code for the whole RankedStat entity?

hard totem
#

Sure, here's the RankedStat-Entity:

import { Prop, Schema, SchemaFactory, Virtual } from '@nestjs/mongoose';
import { Document, HydratedDocument } from 'mongoose';

export type RankedStatDocument = HydratedDocument<RankedStat>;

@Schema({ _id: false, toJSON: { virtuals: true }, toObject: { virtuals: true } })
export class RankedStat extends Document<string, null, RankedStat> {
    @Prop({ required: true })
    tier: string;

    @Prop({ required: true })
    wins: number;

    @Prop({ required: true })
    losses: number;

    /* @Virtual({
        get: function (this: RankedStat) {
            return (this.wins / (this.wins + this.losses)) * 100;
        }
    })
    winRate: number; */
}

export const RankedStatSchema = SchemaFactory.createForClass(RankedStat);
#

I've commented the Virtual-Decorater, as it seemed that it doesn't have any effect on SubDocuments

hard totem
#

In case you ask for it, here's the User-Entity:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, HydratedDocument, Types } from 'mongoose';
import { RankedStat } from './ranked-stat.schema';

export type UserDocumentOverride = {
    name: Types.DocumentArray<RankedStat>;
}

export type UserDocument = HydratedDocument<User, UserDocumentOverride>;

@Schema({ id: false, timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } })
export class User extends Document<string, null, User> {
    static get modelName() {
        return 'User';
    }

    @Prop({ required: true, unique: true })
    username: string;

    @Prop({ required: true, min: 0 })
    level: number;

    @Prop({ required: true })
    iconId: number;

    @Prop({ type: [RankedStat] })
    rankedStats: RankedStat[];
}

export const UserSchema = SchemaFactory.createForClass(User);

// I have to use the code below, the Virtual-Decorator is not working
UserSchema.path('rankedStats').schema.virtual('totalGames').get(function(this: RankedStat) {
    return this.wins + this.losses;
});

UserSchema.path('rankedStats').schema.virtual('winRate').get(function(this: RankedStat) {
    return (this.wins / (this.wins + this.losses)) * 100;
});
hearty coral
#

Is there a reason why you are extending from Document? I believe that isn't necessary, especially since you are creating the RankedStatDocument type. Not sure that will fix this problem, but it might.

hard totem
#

Doing this as I use CASL and the _id Property is otherwise not available in my code anywhere.. 🤔 Added this, so my IDE knows that _id is a valid property

#

I‘ll remove it, if I work on my code again but I am not sure that this will solve my issue.

If you add Virtuals with the Decorator on the parent Document, you can use console.log(UserSchema.virtuals) and you‘ll see it there. If you add it in the SubDocument and use console.log(UserSchema.path('rankedStats').schema.virtuals) then the object is just empty.

Anyway, I‘ll try it out and add some feedback here in a few hours.

Thanks, that you try to help me 🙂

hearty coral
#

If you need the _id property, just add it without the prop decorator. It should be available.

hard totem
#

Have done this too, but extending seems for me the cleaner solution. Anyway, I‘ve seen that we could speak the same language - would this be allowed here?

hearty coral
#

Not sure what you mean by language? German?

hard totem
#

Yep

hearty coral
#

No. We'd need to move to the German channel. And, to be honest, my tech German isn't that great. It's more Denglish than Deutsch. 😛

hard totem
#

No problem 😁 for me it‘s a bit easier to explain things.

Another solution instead of extending the Schema Model would be the use of UserDocument type - but I‘m not sure if CASL will still work.

I‘ll change my code in like an hour, so I can give you better feedback 🙂

hearty coral
#

You also noted at the beginning, you wish to stick with Nest's Mongoose module. Is there a particular reason? I can definitely highly recommend using Typegoose, as it covers a lot more in terms of TypeScript. To me, Nest's module is a "Krücke" .

hard totem
#

😂😂 Okey, this was pretty funny. Honestly, there‘s no reason. It‘s just because the NestJS Documentation explains everything by using the @nestjs/mongoose module.

My project is a small hobby project for now, and I‘m using NestJS the first time after developing my first expressjs restapi using vanilla JS.

It‘s because I don‘t know how much effort I need to put in. Oh and honestly I wasn‘t informed at the beginning. Thought Typegoose means moving away from MongoDB but after small research I figured out that the name already tells me, that Mongoose is used in the background 😁

hearty coral
#

Yeah, Typegoose is TypeScript and Mongoose put together. 🙂 Typegoose has better documentation than what Nest has and as I said, is more "TypeScript" based. It does a lot more with TS than Nest's module does, which makes you often revert down to regular Mongoose to get some things done or even have to find workarounds. This might even be one of them, as you are finding. 🤷🏻

#

Also, the Typegoose maintainer is German. 🙂 And he's really, really good at TypeScript and very on-top of that library. He is also the maintainer of the Mongo Memory Server, which means he knows both worlds very well.

#

I even tried to get the founder of Nest to drop the Nest module in favor of the Typegoose module, and basically make it third party, much like TypeORM, but he didn't go for it. 🤷🏻

#

As for additional work to move to Typegoose, it shouldn't be that hard, especially if you just started out with your project.