Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

isRefType function fails to identify wrapped ObjectId instances #924

Closed
manzonif opened this issue Apr 2, 2024 · 10 comments
Closed

isRefType function fails to identify wrapped ObjectId instances #924

manzonif opened this issue Apr 2, 2024 · 10 comments
Labels
bug Something isn't working info needed Extra information is needed

Comments

@manzonif
Copy link

manzonif commented Apr 2, 2024

Versions

NodeJS: 20.11.1
Typegoose (NPM): 12.0.0
mongoose: 8.0.3
mongodb: MongoDB

What is the Problem?

The isRefType function in Typegoose seems to fail to identify ObjectId instances that are "wrapped" (boxed) by MongoDB. When checking if a document property is an ObjectId reference type, the isRefType function returns false even though the property appears to be a valid ObjectId.

Code Example

import { isRefType } from '@typegoose/typegoose';
import mongoose from 'mongoose';

const doc = { company: new ObjectId("63a29391016fe22c0045abb9") };

console.log(mongoose.Types.ObjectId.isValid(doc.company)); // true
console.log(isRefType(doc.company, mongoose.Types.ObjectId)); // false (expected true)

Do you know why it happens?

The issue appears to be caused by the way MongoDB "wraps" (boxes) ObjectId instances when returning them from the database. The isRefType function checks if the document property is an instance of mongoose.Types.ObjectId using the instanceof operator. However, the wrapped ObjectId instance is not a direct instance of the ObjectId class, so the instanceof check fails.

One potential solution could be to modify the isRefType function to handle these wrapped ObjectId instances. For example, by checking if the object has a _bsontype property with the value 'ObjectID', which is a known characteristic of wrapped ObjectId instances.

I would appreciate any guidance or insights you can provide on this issue and the appropriate way to handle wrapped ObjectId instances in Typegoose.

@manzonif manzonif added the bug Something isn't working label Apr 2, 2024
@hasezoey

This comment was marked as outdated.

@hasezoey hasezoey added the info needed Extra information is needed label Apr 2, 2024
@hasezoey
Copy link
Member

hasezoey commented Apr 2, 2024

sorry for the earlier message, i didnt read correctly.

yes, currently isRefType checks with a instanceof, which should be correct, but will fail if it is a instance of (mongodb|bson).ObjectId.
could you provide where you get the new ObjectId from? (as it is not listed in the imports)

@manzonif
Copy link
Author

manzonif commented Apr 2, 2024

I get it fron nestjs resolveField:

  @ResolveField(returns => Company)
  company(
    @Parent() news: News,
    @Args('lang') lang: string,
    @Loader(CompanyDataLoader.name)
    companyDataLoader: Dataloader<IntlDataLoaderKey<Company>, Company>,
  ): Promise<Company> {
    const doc: any = news.company; //actually company is:  company?: Ref<Company>; in the news model.
    ```

@hasezoey
Copy link
Member

hasezoey commented Apr 2, 2024

I get it fron nestjs resolveField:

thanks, but i do not really understand what is coming from where, is the ObjectId in your case a mongodb.ObjectId (or bson.ObjectId) or something different?
also is news in your case a POJO (like gotten directly from JSON, and only contains JSON compatible types), instance of News (without further information i assume is a typegoose class), a mongoose Document, or a mongodb "document"?
(Note that the example provided in the issue description is basically a mongodb "document", if the type there is a mongodb.ObjectId)

Note: i mean the types at runtime not just typescript types

@manzonif
Copy link
Author

manzonif commented Apr 2, 2024

Mongoose Document.

const news = await this.newsModel.findOne<NewsDocument>(criteria);

@hasezoey
Copy link
Member

hasezoey commented Apr 2, 2024

Mongoose Document.

then the check should work without problems, to my knowledge at least; unless you somehow have mis-matching mongoose versions installed?

@manzonif
Copy link
Author

manzonif commented Apr 2, 2024

I need to debug it and see how it came from, under the hood. This issue starts after several library updates. I will check more thoroughly.

@manzonif
Copy link
Author

manzonif commented Apr 2, 2024

Hard to say. Basically, if the id comes from mongoose's Document.get, I always have doc instanceof mongoose.Types.ObjectId = false, if instead it comes as a query parameter I have true.
Screenshot 2024-04-02 200903
Screenshot 2024-04-02 200929
Screenshot 2024-04-02 203240
When it came from here.
Screenshot 2024-04-02 202543

In fact the query is not the one I reported before, but rather this:

query = (this._model as SoftDeleteModel<T>).find(conditions).sort(sort).limit(limit);
for (let doc: Document = await cursor.next(); doc != null; doc = await cursor.next()) {
....

Maybe something related to cursor.next()?

@hasezoey
Copy link
Member

hasezoey commented Apr 3, 2024

Guessing from the screenshots, i think those ObjectIds are a direct instance of mongodb.ObjectId(/bson.ObjectId).

I dont quite know the semantics of cursor, but in your case either your code is "wrong", or you are seemingly directly using mongodb's query instead of mongoose.
The reason for this is that in mongoose you dont have a .next function on a query until you call .cursor(), whereas mongodb's query always has a .next() as it is already a cursor.

Maybe check that the returned doc is actually a doc instanceof mongoose.Document and also try yourObjectId instanceof mongodb.ObjectId (or if you dont have mongodb directly installed yourObjectId instanceof mongoose.mongo.ObjectId).
Also try to check what this._model is on runtime. (example: this._model.prototype instanceof mongoose.Model should return true)

@manzonif
Copy link
Author

manzonif commented Apr 3, 2024

You are right! I finally found the problem.

@Injectable()
export class TypegooseInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(map(data => {
      if (Array.isArray(data)) {
        return data.map(item => {
          return item instanceof Model ? toGraph(item) : item;
        });
      }
      // **this fix the problem**
      if (Array.isArray(data.edges)) {
        for (const edge of data.edges) {
          edge.node = edge.node  ? toGraph(edge.node) : edge.node;
        }
      }
      if (data instanceof Model) {
        return toGraph(data);
      }

      return data;
    }));
  }
}

function toGraph(doc: Document): any {
  const convertedDocument = doc.toObject();
  const DocumentClass = getClass(doc);
  Object.setPrototypeOf(convertedDocument, DocumentClass.prototype);
  return convertedDocument;
}

Thank you so much for your help. Sorry, but sometimes it's pure hell.

@manzonif manzonif closed this as completed Apr 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working info needed Extra information is needed
Projects
None yet
Development

No branches or pull requests

2 participants